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

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


The following commit(s) were added to refs/heads/master by this push:
     new f57c2285ec2 Validate segment assignment for strict replica-group 
routing (#20006)
f57c2285ec2 is described below

commit f57c2285ec259041114eca09d11f6a882d9dcb99
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Oct 23 11:03:19 2026 -0700

    Validate segment assignment for strict replica-group routing (#20006)
---
 .../api/resources/TableConfigValidationUtils.java  |  45 +++-
 .../resources/TableConfigValidationUtilsTest.java  |  76 ++++++
 .../segment/local/utils/TableConfigUtils.java      | 124 ++++++++-
 .../segment/local/utils/TableConfigUtilsTest.java  | 292 +++++++++++++++++++--
 4 files changed, 500 insertions(+), 37 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
index 4c7f7a6fd5b..19692575c01 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
@@ -19,7 +19,9 @@
 package org.apache.pinot.controller.api.resources;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import java.util.List;
+import java.util.Map;
 import javax.annotation.Nullable;
 import org.apache.helix.model.InstanceConfig;
 import org.apache.pinot.common.assignment.InstanceAssignmentConfigUtils;
@@ -44,7 +46,7 @@ import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 ///
 /// This lives in `pinot-controller` (not `pinot-segment-local`'s 
`TableConfigUtils`)
 /// because validation requires controller-level dependencies like 
[PinotHelixResourceManager],
-/// [ControllerConf], [PinotTaskManager], and [TableRebalancer].
+/// [ControllerConf], [PinotTaskManager], and [InstanceAssignmentUtils].
 public final class TableConfigValidationUtils {
 
   private TableConfigValidationUtils() {
@@ -121,9 +123,10 @@ public final class TableConfigValidationUtils {
   }
 
   /// Computes the instance partitions the table would get on creation, so 
that an instance assignment that cannot be
-  /// satisfied by the current instances is rejected before the table config 
is persisted, and validates that the
-  /// OFFLINE segments are placed by partition when the OFFLINE instance 
partitions have multiple partitions. This
-  /// covers pre-configured instance partitions, whose layout is not visible 
in the table config.
+  /// satisfied by the current instances is rejected before the table config 
is persisted, and validates them: the
+  /// OFFLINE segments must be placed by partition when the OFFLINE instance 
partitions have multiple partitions, and
+  /// the instance partitions must fit strict replica-group routing. This 
covers pre-configured instance partitions,
+  /// whose layout is not visible in the table config.
   private static void validateInstanceAssignment(PinotHelixResourceManager 
resourceManager, TableConfig tableConfig) {
     List<InstancePartitionsType> instancePartitionsTypes;
     if (tableConfig.getTableType() == TableType.OFFLINE) {
@@ -135,6 +138,7 @@ public final class TableConfigValidationUtils {
     }
     List<InstanceConfig> instanceConfigs = 
resourceManager.getAllHelixInstanceConfigs();
     InstancePartitions offlineInstancePartitions = null;
+    InstancePartitions consumingInstancePartitions = null;
     try {
       for (InstancePartitionsType instancePartitionsType : 
instancePartitionsTypes) {
         InstancePartitions instancePartitions;
@@ -147,6 +151,8 @@ public final class TableConfigValidationUtils {
         }
         if (instancePartitionsType == InstancePartitionsType.OFFLINE) {
           offlineInstancePartitions = instancePartitions;
+        } else if (instancePartitionsType == InstancePartitionsType.CONSUMING) 
{
+          consumingInstancePartitions = instancePartitions;
         }
       }
     } catch (Exception e) {
@@ -157,5 +163,36 @@ public final class TableConfigValidationUtils {
     if (offlineInstancePartitions != null && 
offlineInstancePartitions.getNumPartitions() > 1) {
       TableConfigUtils.validatePartitionedSegmentAssignment(tableConfig, 
InstancePartitionsType.OFFLINE.name());
     }
+    validateStrictReplicaGroupInstancePartitions(tableConfig, 
offlineInstancePartitions, consumingInstancePartitions);
+  }
+
+  /// Validates the computed or fetched instance partitions of a table using 
strict replica-group routing, which is the
+  /// only place where pre-configured instance partitions can be checked; the 
strict replica-group routing check in
+  /// [TableConfigUtils#validate] covers the instance assignment config. The 
OFFLINE instance partitions must place each
+  /// partition on exactly one instance per replica-group. CONSUMING segments 
are placed by their stream partition, so
+  /// the CONSUMING instance partitions need the same layout only when they 
have multiple partitions. COMPLETED segments
+  /// are never relocated for such tables.
+  @VisibleForTesting
+  static void validateStrictReplicaGroupInstancePartitions(TableConfig 
tableConfig,
+      @Nullable InstancePartitions offlineInstancePartitions,
+      @Nullable InstancePartitions consumingInstancePartitions) {
+    if (!TableConfigUtils.requiresStrictReplicaGroupAssignment(tableConfig)) {
+      return;
+    }
+    if (offlineInstancePartitions != null) {
+      validateOneInstancePerPartition(tableConfig, offlineInstancePartitions);
+    }
+    if (consumingInstancePartitions != null && 
consumingInstancePartitions.getNumPartitions() > 1) {
+      validateOneInstancePerPartition(tableConfig, 
consumingInstancePartitions);
+    }
+  }
+
+  private static void validateOneInstancePerPartition(TableConfig tableConfig, 
InstancePartitions instancePartitions) {
+    for (Map.Entry<String, List<String>> entry : 
instancePartitions.getPartitionToInstancesMap().entrySet()) {
+      Preconditions.checkState(entry.getValue().size() == 1,
+          "Table: %s using strict replica-group routing must have exactly one 
instance per partition in each "
+              + "replica-group, found instances: %s for 
partition_replicaGroup: %s in instance partitions: %s",
+          tableConfig.getTableName(), entry.getValue(), entry.getKey(), 
instancePartitions.getInstancePartitionsName());
+    }
   }
 }
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtilsTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtilsTest.java
index c59b1aab42a..0a1a798572f 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtilsTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtilsTest.java
@@ -18,13 +18,18 @@
  */
 package org.apache.pinot.controller.api.resources;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
+import org.apache.pinot.common.assignment.InstancePartitions;
+import org.apache.pinot.spi.config.table.RoutingConfig;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableCustomConfig;
 import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.Test;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.expectThrows;
 
@@ -73,4 +78,75 @@ public class TableConfigValidationUtilsTest {
     TableConfig tableConfig = tableConfigWithCustomConfig("key", "plainValue");
     TableConfigValidationUtils.validateEnvironmentVariables(tableConfig);
   }
+
+  private static InstancePartitions createInstancePartitions(String name, int 
numPartitions, int numReplicaGroups,
+      int numInstancesPerPartition) {
+    InstancePartitions instancePartitions = new InstancePartitions(name);
+    for (int partitionId = 0; partitionId < numPartitions; partitionId++) {
+      for (int replicaGroupId = 0; replicaGroupId < numReplicaGroups; 
replicaGroupId++) {
+        List<String> instances = new ArrayList<>(numInstancesPerPartition);
+        for (int i = 0; i < numInstancesPerPartition; i++) {
+          instances.add("Server_" + partitionId + "_" + replicaGroupId + "_" + 
i);
+        }
+        instancePartitions.setInstances(partitionId, replicaGroupId, 
instances);
+      }
+    }
+    return instancePartitions;
+  }
+
+  @Test
+  public void testValidateStrictReplicaGroupInstancePartitions() {
+    RoutingConfig strictRoutingConfig =
+        new RoutingConfig(null, null, 
RoutingConfig.STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false);
+    InstancePartitions wideInstancePartitions = 
createInstancePartitions("table_OFFLINE", 4, 2, 2);
+
+    // Tables without strict replica-group routing are not validated
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("table").build();
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
 wideInstancePartitions, null);
+
+    // Dimension tables host every segment on every server
+    tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("table")
+        .setIsDimTable(true)
+        .setRoutingConfig(strictRoutingConfig)
+        .build();
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
 wideInstancePartitions, null);
+
+    // OFFLINE instance partitions with one instance per partition in each 
replica-group
+    tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("table")
+        .setRoutingConfig(strictRoutingConfig)
+        .build();
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
+        createInstancePartitions("table_OFFLINE", 4, 2, 1), null);
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
+        createInstancePartitions("table_OFFLINE", 1, 2, 1), null);
+
+    // OFFLINE instance partitions with multiple instances per partition
+    TableConfig offlineTableConfig = tableConfig;
+    IllegalStateException e = expectThrows(IllegalStateException.class,
+        () -> 
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(offlineTableConfig,
+            wideInstancePartitions, null));
+    assertEquals(e.getMessage(),
+        "Table: table_OFFLINE using strict replica-group routing must have 
exactly one instance per partition in each "
+            + "replica-group, found instances: [Server_0_0_0, Server_0_0_1] 
for partition_replicaGroup: 0_0 in "
+            + "instance partitions: table_OFFLINE");
+
+    // CONSUMING instance partitions with a single partition, or one instance 
per partition
+    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName("table")
+        .setRoutingConfig(strictRoutingConfig)
+        .build();
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
 null,
+        createInstancePartitions("table_CONSUMING", 1, 2, 3));
+    
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(tableConfig,
 null,
+        createInstancePartitions("table_CONSUMING", 4, 2, 1));
+
+    // CONSUMING instance partitions with multiple partitions and multiple 
instances per partition
+    TableConfig realtimeTableConfig = tableConfig;
+    e = expectThrows(IllegalStateException.class,
+        () -> 
TableConfigValidationUtils.validateStrictReplicaGroupInstancePartitions(realtimeTableConfig,
 null,
+            createInstancePartitions("table_CONSUMING", 4, 2, 2)));
+    assertEquals(e.getMessage(),
+        "Table: table_REALTIME using strict replica-group routing must have 
exactly one instance per partition in each "
+            + "replica-group, found instances: [Server_0_0_0, Server_0_0_1] 
for partition_replicaGroup: 0_0 in "
+            + "instance partitions: table_CONSUMING");
+  }
 }
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index b933a997ddb..eb07e0c0640 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -39,6 +39,8 @@ import javax.annotation.Nullable;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.pinot.common.assignment.InstanceAssignmentConfigUtils;
+import org.apache.pinot.common.assignment.InstancePartitionsUtils;
 import org.apache.pinot.common.evaluator.FunctionEvaluatorFactory;
 import org.apache.pinot.common.function.FunctionInfo;
 import org.apache.pinot.common.function.FunctionRegistry;
@@ -225,6 +227,7 @@ public final class TableConfigUtils {
     validateInstancePartitionsTypeMapConfig(tableConfig);
     validatePartitionedReplicaGroupInstance(tableConfig);
     validateInstanceAssignmentConfigs(tableConfig);
+    validateStrictReplicaGroupRouting(tableConfig);
 
     if (!skipTypes.contains(ValidationType.UPSERT)) {
       validateUpsertAndDedupConfig(tableConfig, schema, existingTableConfig);
@@ -1057,12 +1060,15 @@ public final class TableConfigUtils {
 
   /// Validates the upsert- and dedup-related configuration. Returns early 
when neither is enabled; otherwise checks
   /// the shared requirements (upsert and dedup are mutually exclusive, 
primary keys exist and are single-valued,
-  /// strict replica-group routing, no COMPLETED instance partitions, valid 
tenant tag override for REALTIME, OFFLINE
-  /// restrictions) and then, per the enabled mode:
+  /// strict replica-group routing, no tenant tag override for REALTIME, 
OFFLINE restrictions) and then, per the enabled
+  /// mode:
   /// - Upsert: no multi-tier / star-tree, comparison / delete-record / 
out-of-order columns are valid, deleted-keys
   ///   compaction consistency, post-partial-upsert transforms, 
consistency-mode constraints, and delegates to
   ///   [#validateTTLForUpsertConfig] and [#validatePartialUpsertStrategies]; 
rejects MD5 when disabled.
   /// - Dedup: delegates to [#validateTTLForDedupConfig]; rejects MD5 when 
disabled.
+  ///
+  /// The segment assignment requirements shared with strict replica-group 
routing (one instance per partition, no
+  /// COMPLETED segment relocation) are validated by 
[#validateStrictReplicaGroupRouting].
   @VisibleForTesting
   static void validateUpsertAndDedupConfig(TableConfig tableConfig, Schema 
schema) {
     validateUpsertAndDedupConfig(tableConfig, schema, null);
@@ -1102,10 +1108,6 @@ public final class TableConfigUtils {
               
tableConfig.getTenantConfig().getTagOverrideConfig().getRealtimeConsuming() == 
null
                   && 
tableConfig.getTenantConfig().getTagOverrideConfig().getRealtimeCompleted() == 
null),
           "Invalid tenant tag override used for Upsert/Dedup table");
-      Map<String, InstanceAssignmentConfig> instanceAssignmentConfigMap = 
tableConfig.getInstanceAssignmentConfigMap();
-      Preconditions.checkState(instanceAssignmentConfigMap == null || 
!instanceAssignmentConfigMap.containsKey(
-              InstancePartitionsType.COMPLETED.name()),
-          "COMPLETED instance partitions can't be configured for upsert / 
dedup tables");
     } else {
       Preconditions.checkState(!dedupEnabled,
           "Dedup is not supported for OFFLINE table. Only upsert is supported 
for OFFLINE table");
@@ -1569,6 +1571,116 @@ public final class TableConfigUtils {
         partitionColumn);
   }
 
+  /// Validates that a table using strict replica-group routing has a segment 
assignment that places all segments of
+  /// a partition on exactly one instance per replica-group. Strict 
replica-group routing routes all segments with the
+  /// same instances in the ideal state to the same instance, which only 
provides partition level consistency when the
+  /// segment assignment keeps each partition on one instance per 
replica-group. Upsert and dedup tables rely on the
+  /// same property and are validated regardless of the configured instance 
selector.
+  ///
+  /// - OFFLINE: the OFFLINE instance assignment must be replica-group based 
with one instance per partition, and the
+  ///   OFFLINE segment assignment strategy, if configured, must be partition 
based (`replicagroup` or
+  ///   `roundrobinreplicagroup`), since the other strategies ignore the 
partition. The partition column of a
+  ///   partitioned instance assignment is validated by 
[#validatePartitionedSegmentAssignment]. The instance
+  ///   assignment can also come from the deprecated 
`replicaGroupStrategyConfig`, which takes effect only together with
+  ///   the `replicagroup` strategy.
+  /// - REALTIME: CONSUMING segments are placed by their stream partition, 
which keeps each partition on one instance
+  ///   per replica-group when the CONSUMING instance partitions have a single 
partition or one instance per
+  ///   partition. COMPLETED segments must not be relocated, which would split 
each partition between its CONSUMING
+  ///   and COMPLETED instances.
+  ///
+  /// The layout of pre-configured instance partitions lives in ZK and is not 
visible here, so the controller validates
+  /// it on the fetched instance partitions instead. Dimension tables are 
exempt because every server hosts every
+  /// segment.
+  @VisibleForTesting
+  static void validateStrictReplicaGroupRouting(TableConfig tableConfig) {
+    if (!requiresStrictReplicaGroupAssignment(tableConfig)) {
+      return;
+    }
+    if (tableConfig.getTableType() == TableType.OFFLINE) {
+      validateStrictReplicaGroupOfflineInstanceAssignment(tableConfig);
+    } else {
+      validateStrictReplicaGroupRealtimeInstanceAssignment(tableConfig);
+    }
+  }
+
+  /// Returns `true` if the segment assignment of the table must place all 
segments of a partition on exactly one
+  /// instance per replica-group: the table uses strict replica-group routing, 
or is an upsert/dedup table, whose
+  /// allowed instance selectors all rely on this property. Dimension tables 
are excluded because every server hosts
+  /// every segment.
+  public static boolean requiresStrictReplicaGroupAssignment(TableConfig 
tableConfig) {
+    if (tableConfig.isDimTable()) {
+      return false;
+    }
+    if (tableConfig.isUpsertEnabled() || tableConfig.isDedupEnabled()) {
+      return true;
+    }
+    RoutingConfig routingConfig = tableConfig.getRoutingConfig();
+    String instanceSelectorType = routingConfig != null ? 
routingConfig.getInstanceSelectorType() : null;
+    return 
RoutingConfig.STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE.equalsIgnoreCase(instanceSelectorType);
+  }
+
+  /// Validates the OFFLINE instance assignment config for strict 
replica-group routing.
+  private static void 
validateStrictReplicaGroupOfflineInstanceAssignment(TableConfig tableConfig) {
+    SegmentAssignmentConfig segmentAssignmentConfig =
+        
MapUtils.emptyIfNull(tableConfig.getSegmentAssignmentConfigMap()).get(InstancePartitionsType.OFFLINE.name());
+    String assignmentStrategy =
+        segmentAssignmentConfig != null ? 
segmentAssignmentConfig.getAssignmentStrategy() : null;
+    Preconditions.checkState(assignmentStrategy == null
+            || 
AssignmentStrategy.PARTITION_BASED_STRATEGIES.contains(assignmentStrategy.toLowerCase(Locale.ROOT)),
+        "Table using strict replica-group routing can only use '%s' or '%s' 
OFFLINE segment assignment strategy, "
+            + "found: %s", 
AssignmentStrategy.REPLICA_GROUP_SEGMENT_ASSIGNMENT_STRATEGY,
+        
AssignmentStrategy.ROUND_ROBIN_REPLICA_GROUP_SEGMENT_ASSIGNMENT_STRATEGY, 
assignmentStrategy);
+    if 
(InstancePartitionsUtils.hasPreConfiguredInstancePartitions(tableConfig, 
InstancePartitionsType.OFFLINE)) {
+      return;
+    }
+    // The instance assignment config comes from instanceAssignmentConfigMap, 
or from the deprecated
+    // replicaGroupStrategyConfig when the replica-group segment assignment 
strategy is configured
+    Preconditions.checkState(
+        InstanceAssignmentConfigUtils.allowInstanceAssignment(tableConfig, 
InstancePartitionsType.OFFLINE),
+        "Table using strict replica-group routing must have OFFLINE instance 
assignment config "
+            + "(instanceAssignmentConfigMap) to ensure all segments of a 
partition are assigned to the same server");
+    InstanceReplicaGroupPartitionConfig replicaGroupPartitionConfig =
+        InstanceAssignmentConfigUtils.getInstanceAssignmentConfig(tableConfig, 
InstancePartitionsType.OFFLINE)
+            .getReplicaGroupPartitionConfig();
+    Preconditions.checkState(replicaGroupPartitionConfig.isReplicaGroupBased(),
+        "Table using strict replica-group routing must use replica-group based 
OFFLINE instance assignment to ensure "
+            + "all segments of a partition are assigned to the same server");
+    
Preconditions.checkState(getNumInstancesPerPartition(replicaGroupPartitionConfig)
 <= 1,
+        "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+            + "numInstancesPerReplicaGroup when it is not set) in OFFLINE 
instance assignment config to ensure all "
+            + "segments of a partition are assigned to the same server");
+  }
+
+  /// Validates the REALTIME instance assignment config for strict 
replica-group routing.
+  private static void 
validateStrictReplicaGroupRealtimeInstanceAssignment(TableConfig tableConfig) {
+    
Preconditions.checkState(!InstanceAssignmentConfigUtils.shouldRelocateCompletedSegments(tableConfig),
+        "Table using strict replica-group routing must not relocate COMPLETED 
segments (tagOverrideConfig with a "
+            + "different realtimeCompleted tag, 
instanceAssignmentConfigMap.COMPLETED or "
+            + "instancePartitionsMap.COMPLETED) to ensure all segments of a 
partition are assigned to the same server");
+    InstanceAssignmentConfig consumingInstanceAssignmentConfig =
+        
MapUtils.emptyIfNull(tableConfig.getInstanceAssignmentConfigMap()).get(InstancePartitionsType.CONSUMING.name());
+    if (consumingInstanceAssignmentConfig != null) {
+      InstanceReplicaGroupPartitionConfig replicaGroupPartitionConfig =
+          consumingInstanceAssignmentConfig.getReplicaGroupPartitionConfig();
+      
Preconditions.checkState(!replicaGroupPartitionConfig.isReplicaGroupBased()
+              || replicaGroupPartitionConfig.getNumPartitions() <= 1
+              || getNumInstancesPerPartition(replicaGroupPartitionConfig) <= 1,
+          "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+              + "numInstancesPerReplicaGroup when it is not set) in CONSUMING 
instance assignment config with multiple "
+              + "partitions to ensure all segments of a partition are assigned 
to the same server");
+    }
+  }
+
+  /// Returns the number of instances per partition the instance assignment 
config resolves to: the configured
+  /// `numInstancesPerPartition`, or `numInstancesPerReplicaGroup` when it is 
not set, since each partition then gets
+  /// every instance of its replica-group. Returns 0 when neither is set, in 
which case the count depends on the
+  /// instances in the pool and is validated on the computed instance 
partitions by the controller.
+  private static int 
getNumInstancesPerPartition(InstanceReplicaGroupPartitionConfig 
replicaGroupPartitionConfig) {
+    int numInstancesPerPartition = 
replicaGroupPartitionConfig.getNumInstancesPerPartition();
+    return numInstancesPerPartition > 0 ? numInstancesPerPartition
+        : replicaGroupPartitionConfig.getNumInstancesPerReplicaGroup();
+  }
+
   /// Validates the partial upsert-related configurations:
   ///  - Null handling must be enabled
   ///  - Merger cannot be applied to private key columns
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index ed33075921d..ad89745e377 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -3012,33 +3012,6 @@ public class TableConfigUtilsTest {
       assertEquals(e.getMessage(), "Invalid tenant tag override used for 
Upsert/Dedup table");
     }
 
-    // Instance assignment config with just CONSUMING instance partitions 
configured should be allowed
-    InstanceAssignmentConfig instanceAssignmentConfig = 
mock(InstanceAssignmentConfig.class);
-    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
-        .setTimeColumnName(TIME_COLUMN)
-        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
-        .setStreamConfigs(getStreamConfigs())
-        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
-        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(), 
instanceAssignmentConfig))
-        .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
-
-    // Instance assignment config with CONSUMING and COMPLETED instance 
partitions configured should not be allowed
-    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
-        .setTimeColumnName(TIME_COLUMN)
-        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
-        .setStreamConfigs(getStreamConfigs())
-        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
-        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(), 
instanceAssignmentConfig,
-            InstancePartitionsType.COMPLETED.name(), instanceAssignmentConfig))
-        .build();
-    try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
-      fail("Instance assignment config with COMPLETED instance partitions must 
not be allowed with upsert");
-    } catch (IllegalStateException e) {
-      assertEquals(e.getMessage(), "COMPLETED instance partitions can't be 
configured for upsert / dedup tables");
-    }
-
     // empty tag override with upsert should pass
     tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
         .setTimeColumnName(TIME_COLUMN)
@@ -3853,6 +3826,271 @@ public class TableConfigUtilsTest {
   }
 
   @SuppressWarnings("removal")
+  private static InstanceAssignmentConfig 
createReplicaGroupInstanceAssignmentConfig(boolean replicaGroupBased,
+      int numInstancesPerReplicaGroup, int numPartitions, int 
numInstancesPerPartition) {
+    return new InstanceAssignmentConfig(new 
InstanceTagPoolConfig("DefaultTenant_OFFLINE", false, 0, null), null,
+        new InstanceReplicaGroupPartitionConfig(replicaGroupBased, 0, 2, 
numInstancesPerReplicaGroup, numPartitions,
+            numInstancesPerPartition, false, null), null, false);
+  }
+
+  private static TableConfigBuilder 
strictReplicaGroupTableConfigBuilder(TableType tableType) {
+    return new 
TableConfigBuilder(tableType).setTableName(TABLE_NAME).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG);
+  }
+
+  private static void assertStrictReplicaGroupRoutingFails(TableConfig 
tableConfig, String expectedMessage) {
+    IllegalStateException e = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig));
+    assertEquals(e.getMessage(), expectedMessage);
+  }
+
+  @SuppressWarnings("removal")
+  @Test
+  public void testValidateStrictReplicaGroupRouting() {
+    String offlineKey = InstancePartitionsType.OFFLINE.name();
+    SegmentPartitionConfig segmentPartitionConfig =
+        new SegmentPartitionConfig(Map.of("myCol", new 
ColumnPartitionConfig("murmur", 4)));
+    Map<String, InstanceAssignmentConfig> offlineInstanceAssignmentConfigMap =
+        Map.of(offlineKey, createReplicaGroupInstanceAssignmentConfig(true, 0, 
4, 1));
+    SegmentAssignmentConfig replicaGroupSegmentAssignmentConfig =
+        new 
SegmentAssignmentConfig(AssignmentStrategy.REPLICA_GROUP_SEGMENT_ASSIGNMENT_STRATEGY,
 null);
+    SegmentAssignmentConfig balancedSegmentAssignmentConfig =
+        new 
SegmentAssignmentConfig(AssignmentStrategy.BALANCE_NUM_SEGMENT_ASSIGNMENT_STRATEGY,
 null);
+
+    // Tables without strict replica-group routing are not validated
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setRoutingConfig(new RoutingConfig(null, null, 
RoutingConfig.REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // Dimension tables host every segment on every server
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setIsDimTable(true)
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // OFFLINE table with partitioned replica-group instance assignment and 
one instance per partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(offlineInstanceAssignmentConfigMap)
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // OFFLINE table with the replica-group segment assignment strategy 
configured explicitly
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(offlineInstanceAssignmentConfigMap)
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
replicaGroupSegmentAssignmentConfig))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // OFFLINE table with the round-robin replica-group strategy, which is the 
same as replica-group with one instance
+    // per partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(offlineInstanceAssignmentConfigMap)
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey,
+            new 
SegmentAssignmentConfig(AssignmentStrategy.ROUND_ROBIN_REPLICA_GROUP_SEGMENT_ASSIGNMENT_STRATEGY,
+                null)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // Without numInstancesPerPartition, each partition gets 
numInstancesPerReplicaGroup instances, and the layout is
+    // left to the controller when neither is set
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(true, 1, 0, 0)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(true, 0, 0, 0)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(true, 2, 0, 0)))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+            + "numInstancesPerReplicaGroup when it is not set) in OFFLINE 
instance assignment config to ensure all "
+            + "segments of a partition are assigned to the same server");
+
+    // OFFLINE table with a single partition keeps every segment on one 
instance per replica-group
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(true, 0, 0, 1)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // OFFLINE table without OFFLINE instance assignment config
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have OFFLINE instance 
assignment config "
+            + "(instanceAssignmentConfigMap) to ensure all segments of a 
partition are assigned to the same server");
+
+    // OFFLINE table with the deprecated replicaGroupStrategyConfig, which 
takes effect only together with the
+    // replica-group segment assignment strategy
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setReplicaGroupStrategyConfig(new ReplicaGroupStrategyConfig("myCol", 
1))
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
replicaGroupSegmentAssignmentConfig))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setReplicaGroupStrategyConfig(new ReplicaGroupStrategyConfig(null, 1))
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
replicaGroupSegmentAssignmentConfig))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setReplicaGroupStrategyConfig(new ReplicaGroupStrategyConfig("myCol", 
2))
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
replicaGroupSegmentAssignmentConfig))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+            + "numInstancesPerReplicaGroup when it is not set) in OFFLINE 
instance assignment config to ensure all "
+            + "segments of a partition are assigned to the same server");
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setReplicaGroupStrategyConfig(new ReplicaGroupStrategyConfig("myCol", 
1))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have OFFLINE instance 
assignment config "
+            + "(instanceAssignmentConfigMap) to ensure all segments of a 
partition are assigned to the same server");
+
+    // OFFLINE table with pre-configured instance partitions leaves the layout 
check to the controller, but still
+    // cannot use a segment assignment strategy that ignores the partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstancePartitionsMap(Map.of(InstancePartitionsType.OFFLINE, 
"test_OFFLINE"))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setInstancePartitionsMap(Map.of(InstancePartitionsType.OFFLINE, 
"test_OFFLINE"))
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
balancedSegmentAssignmentConfig))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing can only use 'replicagroup' 
or 'roundrobinreplicagroup' OFFLINE "
+            + "segment assignment strategy, found: balanced");
+
+    // OFFLINE table with non replica-group based instance assignment
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(false, 0, 4, 1)))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must use replica-group based 
OFFLINE instance assignment to ensure "
+            + "all segments of a partition are assigned to the same server");
+
+    // OFFLINE table with multiple instances per partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(Map.of(offlineKey, 
createReplicaGroupInstanceAssignmentConfig(true, 0, 4, 2)))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+            + "numInstancesPerReplicaGroup when it is not set) in OFFLINE 
instance assignment config to ensure all "
+            + "segments of a partition are assigned to the same server");
+
+    // OFFLINE table with a segment assignment strategy that ignores the 
partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.OFFLINE)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(offlineInstanceAssignmentConfigMap)
+        .setSegmentAssignmentConfigMap(Map.of(offlineKey, 
balancedSegmentAssignmentConfig))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing can only use 'replicagroup' 
or 'roundrobinreplicagroup' OFFLINE "
+            + "segment assignment strategy, found: balanced");
+
+    // REALTIME table without COMPLETED segment relocation places every 
partition on one instance per replica-group
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(),
+            createReplicaGroupInstanceAssignmentConfig(true, 0, 0, 0)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // REALTIME table with partitioned CONSUMING instance assignment needs one 
instance per partition
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(),
+            createReplicaGroupInstanceAssignmentConfig(true, 0, 4, 1)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(),
+            createReplicaGroupInstanceAssignmentConfig(true, 1, 4, 0)))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.CONSUMING.name(),
+            createReplicaGroupInstanceAssignmentConfig(true, 0, 4, 2)))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have one instance per 
partition (numInstancesPerPartition, or "
+            + "numInstancesPerReplicaGroup when it is not set) in CONSUMING 
instance assignment config with multiple "
+            + "partitions to ensure all segments of a partition are assigned 
to the same server");
+
+    // REALTIME table with tag override that does not relocate COMPLETED 
segments
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        .setTagOverrideConfig(new TagOverrideConfig(null, null))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        .setTagOverrideConfig(new TagOverrideConfig("T1_REALTIME", 
"T1_REALTIME"))
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+
+    // REALTIME table relocating COMPLETED segments through tag override, 
instance assignment config or pre-configured
+    // instance partitions
+    String relocationMessage =
+        "Table using strict replica-group routing must not relocate COMPLETED 
segments (tagOverrideConfig with a "
+            + "different realtimeCompleted tag, 
instanceAssignmentConfigMap.COMPLETED or "
+            + "instancePartitionsMap.COMPLETED) to ensure all segments of a 
partition are assigned to the same server";
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        .setTagOverrideConfig(new TagOverrideConfig("T1_REALTIME", 
"T2_REALTIME"))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig, relocationMessage);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        
.setInstanceAssignmentConfigMap(Map.of(InstancePartitionsType.COMPLETED.name(),
+            createReplicaGroupInstanceAssignmentConfig(true, 0, 4, 1)))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig, relocationMessage);
+    tableConfig = strictReplicaGroupTableConfigBuilder(TableType.REALTIME)
+        .setInstancePartitionsMap(Map.of(InstancePartitionsType.COMPLETED, 
"test_COMPLETED"))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig, relocationMessage);
+
+    // Upsert and dedup tables are validated regardless of the instance 
selector
+    RoutingConfig multiStageReplicaGroupRoutingConfig =
+        new RoutingConfig(null, null, 
RoutingConfig.MULTI_STAGE_REPLICA_GROUP_SELECTOR_TYPE, false);
+    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+        .setRoutingConfig(multiStageReplicaGroupRoutingConfig)
+        .setTagOverrideConfig(new TagOverrideConfig("T1_REALTIME", 
"T2_REALTIME"))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig, relocationMessage);
+    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setDedupConfig(new DedupConfig())
+        .setRoutingConfig(multiStageReplicaGroupRoutingConfig)
+        .setInstancePartitionsMap(Map.of(InstancePartitionsType.COMPLETED, 
"test_COMPLETED"))
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig, relocationMessage);
+    tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+        .setRoutingConfig(multiStageReplicaGroupRoutingConfig)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .build();
+    assertStrictReplicaGroupRoutingFails(tableConfig,
+        "Table using strict replica-group routing must have OFFLINE instance 
assignment config "
+            + "(instanceAssignmentConfigMap) to ensure all segments of a 
partition are assigned to the same server");
+    tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
+        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+        .setRoutingConfig(multiStageReplicaGroupRoutingConfig)
+        .setSegmentPartitionConfig(segmentPartitionConfig)
+        .setInstanceAssignmentConfigMap(offlineInstanceAssignmentConfigMap)
+        .build();
+    TableConfigUtils.validateStrictReplicaGroupRouting(tableConfig);
+  }
+
   @Test
   public void testValidatePartitionedReplicaGroupInstance() {
     String partitionColumn = "testPartitionCol";


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

Reply via email to