This is an automated email from the ASF dual-hosted git repository.
gortiz 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 88ea694bae5 [#12507] Implement adaptive pool selection for
StrictReplicaGroupInstanceSelector (#18947)
88ea694bae5 is described below
commit 88ea694bae5f98760fb8c61f737fe6d3bc713427
Author: Timothy Elgersma <[email protected]>
AuthorDate: Tue Sep 1 02:54:58 2026 -0400
[#12507] Implement adaptive pool selection for
StrictReplicaGroupInstanceSelector (#18947)
---
.../instanceselector/BalancedInstanceSelector.java | 5 +-
.../instanceselector/BaseInstanceSelector.java | 24 +-
.../routing/instanceselector/InstanceSelector.java | 22 +-
.../instanceselector/InstanceSelectorFactory.java | 23 +-
.../MultiStageReplicaGroupSelector.java | 14 +-
.../ReplicaGroupInstanceSelector.java | 39 +-
.../instanceselector/SegmentInstanceCandidate.java | 2 +-
.../StrictReplicaGroupInstanceSelector.java | 62 +-
.../routing/manager/BaseBrokerRoutingManager.java | 6 +-
.../instanceselector/InstanceSelectorTest.java | 90 +-
.../instanceselector/ReplicaGroupSelectorTest.java | 1063 ++++++++++++++++++++
.../apache/pinot/spi/utils/CommonConstants.java | 7 +
pom.xml | 2 +-
13 files changed, 1214 insertions(+), 145 deletions(-)
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BalancedInstanceSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BalancedInstanceSelector.java
index 7707366ee22..a1a7ce09bf2 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BalancedInstanceSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BalancedInstanceSelector.java
@@ -21,7 +21,6 @@ package org.apache.pinot.broker.routing.instanceselector;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.apache.commons.lang3.tuple.Pair;
import
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
import org.apache.pinot.common.metrics.BrokerMeter;
import org.apache.pinot.common.metrics.BrokerMetrics;
@@ -44,7 +43,7 @@ import org.apache.pinot.common.utils.HashUtil;
public class BalancedInstanceSelector extends BaseInstanceSelector {
@Override
- public Pair<Map<String, String>, Map<String, String>> select(List<String>
segments, int requestId,
+ public InstanceMapping select(List<String> segments, int requestId,
SegmentStates segmentStates, Map<String, String> queryOptions) {
Map<String, String> segmentToSelectedInstanceMap = new
HashMap<>(HashUtil.getHashMapCapacity(segments.size()));
// No need to adjust this map per total segment numbers, as optional
segments should be empty most of the time.
@@ -87,6 +86,6 @@ public class BalancedInstanceSelector extends
BaseInstanceSelector {
_brokerMetrics.addMeteredValue(BrokerMeter.POOL_SEG_QUERIES,
entry.getValue(),
BrokerMetrics.getTagForPreferredPool(queryOptions),
String.valueOf(entry.getKey()));
}
- return Pair.of(segmentToSelectedInstanceMap, optionalSegmentToInstanceMap);
+ return new InstanceMapping(segmentToSelectedInstanceMap,
optionalSegmentToInstanceMap);
}
}
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java
index f322325519d..6c8d96cbf6d 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java
@@ -30,7 +30,6 @@ import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.annotation.Nullable;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.AccessOption;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState;
@@ -517,15 +516,14 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
(brokerRequest.getPinotQuery() != null &&
brokerRequest.getPinotQuery().getQueryOptions() != null)
? brokerRequest.getPinotQuery().getQueryOptions() : Map.of();
int requestIdInt = (int) (requestId % MAX_REQUEST_ID);
- // Copy the volatile reference so that segmentToInstanceMap and
unavailableSegments can have a consistent view of
- // the state.
+ // Copy the volatile reference so that the segment-to-instance map and
unavailable segments have a consistent view
+ // of the state.
SegmentStates segmentStates = _segmentStates;
- Pair<Map<String, String>, Map<String, String>> segmentToInstanceMap =
- select(segments, requestIdInt, segmentStates, queryOptions);
+ InstanceMapping mapping = select(segments, requestIdInt, segmentStates,
queryOptions);
Set<String> unavailableSegments = segmentStates.getUnavailableSegments();
if (unavailableSegments.isEmpty()) {
- return new SelectionResult(segmentToInstanceMap, List.of(), 0);
+ return new SelectionResult(mapping, List.of(), 0);
} else {
List<String> unavailableSegmentsForRequest = new ArrayList<>();
for (String segment : segments) {
@@ -533,7 +531,7 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
unavailableSegmentsForRequest.add(segment);
}
}
- return new SelectionResult(segmentToInstanceMap,
unavailableSegmentsForRequest, 0);
+ return new SelectionResult(mapping, unavailableSegmentsForRequest, 0);
}
}
@@ -555,10 +553,10 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
return pool;
}
- /// Selects the server instances for the given segments based on the request
id and segment states. Returns two maps
- /// from segment to selected server instance hosting the segment. The 2nd
map is for optional segments. The optional
- /// segments are used to get the new segments that are not online yet.
Instead of simply skipping them by broker at
- /// routing time, we can send them to servers and let servers decide how to
handle them.
- protected abstract Pair<Map<String, String>, Map<String, String>/*optional
segments*/> select(List<String> segments,
- int requestId, SegmentStates segmentStates, Map<String, String>
queryOptions);
+ /// Selects the server instances for the given segments based on the request
id and segment states. Returns an
+ /// [InstanceSelector.InstanceMapping] containing two maps from segment to
selected server instance. The optional map
+ /// covers new segments that are not online yet; instead of simply skipping
them at the broker, we can send them to
+ /// servers and let servers decide how to handle them.
+ protected abstract InstanceMapping select(List<String> segments, int
requestId, SegmentStates segmentStates,
+ Map<String, String> queryOptions);
}
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelector.java
index 06491ebd90d..5dd2d4f429e 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelector.java
@@ -23,7 +23,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState;
import org.apache.helix.store.zk.ZkHelixPropertyStore;
@@ -88,27 +87,38 @@ public interface InstanceSelector {
return null;
}
+ /// Holds the result of an instance selection: `segmentToInstanceMap` maps
each segment to its selected server
+ /// instance, and `optionalSegmentToInstanceMap` maps segments not yet fully
online that the server may skip.
+ record InstanceMapping(Map<String, String> segmentToInstanceMap,
+ Map<String, String> optionalSegmentToInstanceMap) {
+ static final InstanceMapping EMPTY = new InstanceMapping(Map.of(),
Map.of());
+ }
+
class SelectionResult {
- private final Pair<Map<String, String>, Map<String, String>/*optional
segments*/> _segmentToInstanceMap;
+ private final InstanceMapping _instanceMapping;
private final List<String> _unavailableSegments;
private int _numPrunedSegments;
- public SelectionResult(Pair<Map<String, String>, Map<String, String>>
segmentToInstanceMap,
+ public SelectionResult(InstanceMapping instanceMapping,
List<String> unavailableSegments, int numPrunedSegments) {
- _segmentToInstanceMap = segmentToInstanceMap;
+ _instanceMapping = instanceMapping;
_unavailableSegments = unavailableSegments;
_numPrunedSegments = numPrunedSegments;
}
+ public static SelectionResult empty(int numPrunedSegments) {
+ return new SelectionResult(InstanceMapping.EMPTY, List.of(),
numPrunedSegments);
+ }
+
/// Returns the map from segment to selected server instance hosting the
segment.
public Map<String, String> getSegmentToInstanceMap() {
- return _segmentToInstanceMap.getLeft();
+ return _instanceMapping.segmentToInstanceMap();
}
/// Returns the map from optional segment to selected server instance
hosting the optional segment.
/// Optional segments can be skipped by broker or server upon any issue
w/o failing the query.
public Map<String, String> getOptionalSegmentToInstanceMap() {
- return _segmentToInstanceMap.getRight();
+ return _instanceMapping.optionalSegmentToInstanceMap();
}
/// Returns the unavailable segments (no enabled instance or all enabled
instances are in ERROR state).
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorFactory.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorFactory.java
index 49536b93a8f..79361106aba 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorFactory.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorFactory.java
@@ -74,6 +74,7 @@ public class InstanceSelectorFactory {
ExternalView externalView, Set<String> onlineSegments) {
String tableNameWithType = tableConfig.getTableName();
RoutingConfig routingConfig = tableConfig.getRoutingConfig();
+ AdaptiveServerSelector effectiveAdaptiveServerSelector =
adaptiveServerSelector;
boolean useFixedReplica =
brokerConfig.getProperty(CommonConstants.Broker.CONFIG_OF_USE_FIXED_REPLICA,
CommonConstants.Broker.DEFAULT_USE_FIXED_REPLICA);
if (routingConfig != null && routingConfig.getUseFixedReplica() != null) {
@@ -106,6 +107,19 @@ public class InstanceSelectorFactory {
}
case RoutingConfig.STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE: {
LOGGER.info("Using StrictReplicaGroupInstanceSelector for table:
{}", tableNameWithType);
+ boolean enableStrictReplicaGroupAdaptiveRouting =
brokerConfig.getProperty(
+
CommonConstants.Broker.AdaptiveServerSelector.CONFIG_OF_STRICT_REPLICA_GROUP_ENABLED,
+
CommonConstants.Broker.AdaptiveServerSelector.DEFAULT_STRICT_REPLICA_GROUP_ENABLED);
+ if (!enableStrictReplicaGroupAdaptiveRouting &&
effectiveAdaptiveServerSelector != null) {
+ LOGGER.info("Adaptive routing disabled for
StrictReplicaGroupInstanceSelector table: {}",
+ tableNameWithType);
+ effectiveAdaptiveServerSelector = null;
+ }
+ if (useFixedReplica && effectiveAdaptiveServerSelector != null) {
+ LOGGER.warn("Disabling adaptive routing for
StrictReplicaGroupInstanceSelector table {} using fixed "
+ + "replica routing", tableNameWithType);
+ effectiveAdaptiveServerSelector = null;
+ }
instanceSelector = new StrictReplicaGroupInstanceSelector();
break;
}
@@ -136,8 +150,15 @@ public class InstanceSelectorFactory {
if (instanceSelector == null) {
instanceSelector = new BalancedInstanceSelector();
}
+ if (instanceSelector.getClass() == ReplicaGroupInstanceSelector.class
+ && (tableConfig.isUpsertEnabled() || tableConfig.isDedupEnabled())
+ && effectiveAdaptiveServerSelector != null) {
+ LOGGER.warn("Disabling adaptive routing for legacy upsert/dedup table {}
using ReplicaGroupInstanceSelector",
+ tableNameWithType);
+ effectiveAdaptiveServerSelector = null;
+ }
- instanceSelector.init(tableConfig, propertyStore, brokerMetrics,
adaptiveServerSelector, clock,
+ instanceSelector.init(tableConfig, propertyStore, brokerMetrics,
effectiveAdaptiveServerSelector, clock,
config, enabledInstances, enabledServerMap, idealState, externalView,
onlineSegments);
return instanceSelector;
}
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/MultiStageReplicaGroupSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/MultiStageReplicaGroupSelector.java
index 06dac8d64bf..fd120eed374 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/MultiStageReplicaGroupSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/MultiStageReplicaGroupSelector.java
@@ -29,7 +29,6 @@ import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState;
import org.apache.helix.store.zk.ZkHelixPropertyStore;
@@ -84,7 +83,7 @@ public class MultiStageReplicaGroupSelector extends
BaseInstanceSelector {
}
@Override
- public Pair<Map<String, String>, Map<String, String>> select(List<String>
segments, int requestId,
+ public InstanceMapping select(List<String> segments, int requestId,
SegmentStates segmentStates, Map<String, String> queryOptions) {
// Create a copy of InstancePartitions to avoid race-condition with
event-listeners above.
InstancePartitions instancePartitions = _instancePartitions;
@@ -107,8 +106,9 @@ public class MultiStageReplicaGroupSelector extends
BaseInstanceSelector {
/// Returns a map from the segmentName to the corresponding server. It tries
to select all servers from the
/// preferredReplicaGroup, but if it fails, it will try to select the
relevant server from other instance partitions.
///
- /// @return A pair of maps, where the first map contains the segments that
are assigned to a server and the second
- /// map contains the segments that are optional (i.e., the server is not
online to serve that segment).
+ /// @return An [InstanceMapping] whose `segmentToInstanceMap` contains the
segments assigned to a server and whose
+ /// `optionalSegmentToInstanceMap` contains segments that are optional
(i.e., the server is not online to serve that
+ /// segment).
/// Example:
/// {
/// "required_segments": {
@@ -120,7 +120,7 @@ public class MultiStageReplicaGroupSelector extends
BaseInstanceSelector {
/// "segment4": "server4"
/// }
/// }
- private Pair<Map<String, String>, Map<String, String>> assign(Set<String>
segments,
+ private InstanceMapping assign(Set<String> segments,
SegmentStates segmentStates, InstancePartitions instancePartitions, int
preferredReplicaId) {
Map<String, Integer> instanceToPartitionMap =
instancePartitions.getInstanceToPartitionIdMap();
Map<String, Set<String>> instanceToSegmentsMap = new HashMap<>();
@@ -221,7 +221,7 @@ public class MultiStageReplicaGroupSelector extends
BaseInstanceSelector {
/// Based on whether the selected instance for the segment is online to
serve that segment or not,
/// this method computes the segments that are optional and the segments
that are not.
- private Pair<Map<String, String>, Map<String, String>>
computeOptionalSegments(
+ private InstanceMapping computeOptionalSegments(
Map<String, String> segmentToSelectedInstanceMap, SegmentStates
segmentStates) {
Map<String, String> segmentsToInstanceMap = new HashMap<>();
@@ -250,7 +250,7 @@ public class MultiStageReplicaGroupSelector extends
BaseInstanceSelector {
}
}
- return Pair.of(segmentsToInstanceMap, optionalSegmentToInstanceMap);
+ return new InstanceMapping(segmentsToInstanceMap,
optionalSegmentToInstanceMap);
}
@VisibleForTesting
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupInstanceSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupInstanceSelector.java
index debc04a1912..493e9555f82 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupInstanceSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupInstanceSelector.java
@@ -19,7 +19,6 @@
package org.apache.pinot.broker.routing.instanceselector;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -27,7 +26,6 @@ import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.collections.MapUtils;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState;
import
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
@@ -73,9 +71,13 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
private static final Logger LOGGER =
LoggerFactory.getLogger(ReplicaGroupInstanceSelector.class);
@Override
- public Pair<Map<String, String>, Map<String, String>> select(List<String>
segments, int requestId,
+ public InstanceMapping select(List<String> segments, int requestId,
SegmentStates segmentStates, Map<String, String> queryOptions) {
- ServerSelectionContext ctx = new ServerSelectionContext(queryOptions,
_config);
+ return selectWithContext(segments, requestId, segmentStates, new
ServerSelectionContext(queryOptions, _config));
+ }
+
+ protected InstanceMapping selectWithContext(List<String> segments, int
requestId,
+ SegmentStates segmentStates, ServerSelectionContext ctx) {
if (_adaptiveServerSelector != null) {
// Adaptive Server Selection is enabled.
List<SegmentInstanceCandidate> candidateServers =
fetchCandidateServersForQuery(segments, segmentStates);
@@ -94,7 +96,7 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
}
}
- private Pair<Map<String, String>, Map<String, String>>
selectServers(List<String> segments, int requestId,
+ protected InstanceMapping selectServers(List<String> segments, int requestId,
SegmentStates segmentStates, @Nullable Map<String, Integer>
serverRankMap, ServerSelectionContext ctx) {
Map<String, String> segmentToSelectedInstanceMap = new
HashMap<>(HashUtil.getHashMapCapacity(segments.size()));
@@ -115,7 +117,7 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
// Round-robin selection (default behavior)
int numCandidates = candidates.size();
- int instanceIdx = (requestId + replicaOffset) % numCandidates;
+ int instanceIdx = Math.floorMod(requestId + replicaOffset,
numCandidates);
SegmentInstanceCandidate selectedInstance = candidates.get(instanceIdx);
if (useFixedReplica) {
// Adaptive Server Selection cannot be used with fixed replica routing.
@@ -124,13 +126,19 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
} else if (MapUtils.isNotEmpty(serverRankMap)) {
// Adaptive Server Selection is enabled.
// Use the instance with the best rank if all servers have stats
populated, else use the round-robin selected
- // instance
- selectedInstance = candidates.stream()
- .anyMatch(candidate ->
!serverRankMap.containsKey(candidate.getInstance()))
- ? selectedInstance
- : candidates.stream()
- .min(Comparator.comparingInt(candidate ->
serverRankMap.get(candidate.getInstance())))
- .orElse(selectedInstance);
+ // instance. As of 8 July 2026, this fallback is unreachable, but new
implementations could require it.
+ int bestRank = Integer.MAX_VALUE;
+ for (SegmentInstanceCandidate candidate : candidates) {
+ Integer rank = serverRankMap.get(candidate.getInstance());
+ if (rank == null) {
+ selectedInstance = candidates.get(instanceIdx);
+ break;
+ }
+ if (rank < bestRank) {
+ bestRank = rank;
+ selectedInstance = candidate;
+ }
+ }
}
poolToSegmentCount.merge(selectedInstance.getPool(), 1, Integer::sum);
@@ -150,7 +158,7 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
_brokerMetrics.addMeteredValue(BrokerMeter.POOL_SEG_QUERIES,
entry.getValue(),
BrokerMetrics.getTagForPreferredPool(ctx.getQueryOptions()),
String.valueOf(entry.getKey()));
}
- return Pair.of(segmentToSelectedInstanceMap, optionalSegmentToInstanceMap);
+ return new InstanceMapping(segmentToSelectedInstanceMap,
optionalSegmentToInstanceMap);
}
private List<SegmentInstanceCandidate>
fetchCandidateServersForQuery(List<String> segments,
@@ -194,7 +202,6 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
_oldSegmentExpectedReplicasMap.clear();
int newSegmentMapCapacity =
HashUtil.getHashMapCapacity(newSegmentCreationTimeMap.size());
_newSegmentStateMap = new HashMap<>(newSegmentMapCapacity);
-
Map<String, Map<String, String>> idealStateAssignment =
idealState.getRecord().getMapFields();
Map<String, Map<String, String>> externalViewAssignment =
externalView.getRecord().getMapFields();
@@ -247,7 +254,6 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
// NOTE: onlineInstances is either a TreeSet or an EmptySet (sorted)
Set<String> onlineInstances = entry.getValue();
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
-
Set<String> unavailableInstances =
unavailableInstancesMap.get(idealStateInstanceStateMap.keySet());
List<SegmentInstanceCandidate> candidates = new
ArrayList<>(onlineInstances.size());
int idealStateReplicaId = 0;
@@ -267,7 +273,6 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
Set<String> onlineInstances = entry.getValue();
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
Map<String, String> sortedIdealStateInstanceStateMap =
convertToSortedMap(idealStateInstanceStateMap);
-
Set<String> unavailableInstances =
unavailableInstancesMap.getOrDefault(idealStateInstanceStateMap.keySet(),
Set.of());
List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentInstanceCandidate.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentInstanceCandidate.java
index f501ca8dfab..631d58853f7 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentInstanceCandidate.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentInstanceCandidate.java
@@ -70,6 +70,6 @@ public class SegmentInstanceCandidate {
@Override
public String toString() {
return "SegmentInstanceCandidate{" + "_instance='" + _instance + '\'' + ",
_online=" + _online + ", _pool=" + _pool
- + '}';
+ + ", _idealStateReplicaId=" + _idealStateReplicaId + '}';
}
}
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java
index b7c6675033d..1f296a27c52 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/StrictReplicaGroupInstanceSelector.java
@@ -18,39 +18,35 @@
*/
package org.apache.pinot.broker.routing.instanceselector;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.helix.model.ExternalView;
import org.apache.helix.model.IdealState;
+import
org.apache.pinot.broker.routing.adaptiveserverselector.ServerSelectionContext;
+import org.apache.pinot.common.utils.config.QueryOptionsUtils;
+
/// Instance selector for strict replica-group routing strategy.
///
-/// ```
-/// The strict replica-group routing strategy always routes the query to the
instances within the same replica-group.
-/// (Note that the replica-group information is derived from the ideal state
of the table, where the instances are
-/// sorted alphabetically in the instance state map, so the replica-groups in
the instance selector might not match the
-/// replica-groups in the instance partitions). The goal of this algorithm is
to ensure that segments from the same
-/// partition are never served from multiple different instances. The
instances in a replica-group should have all the
-/// online segments (segments with ONLINE/CONSUMING instances in the ideal
state and selected by the pre-selector)
-/// available (ONLINE/CONSUMING in the external view) in order to serve
queries. If any segment is unavailable in the
-/// replica-group, we mark the whole replica-group down and not serve queries
with this replica-group.
+/// The strict replica-group routing strategy always routes same-partition
segments to the same instance. During
+/// routing state construction, [#updateSegmentMapsForUpsertTable(IdealState,
ExternalView, Set, Map)] removes from
+/// every segment in a partition any replica that is unavailable for any old
segment in that partition. Consequently,
+/// all same-partition segments have identical, ordered candidate identities.
///
-/// The selection algorithm is the same as {@link
ReplicaGroupInstanceSelector}, and will always evenly distribute the
-/// traffic to all replica-groups that have all online segments available.
+/// Adaptive routing relies on two parent-selector contracts:
+/// - [#selectWithContext(List, int, SegmentStates, ServerSelectionContext)]
obtains one ranking snapshot before
+/// iterating the query's segments; it must not re-rank per segment.
+/// - [#selectServers(List, int, SegmentStates, Map, ServerSelectionContext)]
resolves equal-ranked candidates using
+/// its strictly-less-than (`rank < bestRank`) comparison. It consequently
retains the first candidate in the shared
+/// ordered candidate list.
///
-/// The algorithm relies on the mirror segment assignment from replica-group
segment assignment strategy. With mirror
-/// segment assignment, any server in one replica-group will always have a
corresponding server in other replica-groups
-/// that have the same segments assigned. For example, if S1 is a server in
replica-group 1, and it has mirror server S2
-/// in replica-group 2 and S3 in replica-group 3. All segments assigned to S1
will also be assigned to S2 and S3. In
-/// stable scenario (external view matches ideal state), all segments assigned
to S1 will have the same enabled
-/// instances of [S1, S2, S3] sorted (in alphabetical order). If we always
pick the same index of enabled instances for
-/// all segments, only one of S1, S2, S3 will be picked, and all the segments
are processed by the same server. In
-/// transitioning/error scenario (external view does not match ideal state),
if a segment is down on S1, we mark all
-/// segments with the same assignment ([S1, S2, S3]) down on S1 to ensure that
we always route the segments to the same
-/// replica-group.
+/// Since same-partition old segments have identical ordered candidates, these
contracts ensure they select the same
+/// instance. Changing either contract requires revisiting strict
replica-group routing. Different partitions may
+/// independently choose different replicas.
///
-/// Note that new segments won't be used to exclude instances from serving
when the segment is unavailable.
-/// ```
+/// New segments do not exclude a candidate when that segment is unavailable;
they remain optional so that the broker or
+/// server can skip them if necessary.
public class StrictReplicaGroupInstanceSelector extends
ReplicaGroupInstanceSelector {
@Override
@@ -58,4 +54,24 @@ public class StrictReplicaGroupInstanceSelector extends
ReplicaGroupInstanceSele
Map<String, Long> newSegmentCreationTimeMap) {
super.updateSegmentMapsForUpsertTable(idealState, externalView,
onlineSegments, newSegmentCreationTimeMap);
}
+
+ @Override
+ public InstanceMapping select(List<String> segments, int requestId,
+ SegmentStates segmentStates, Map<String, String> queryOptions) {
+ ServerSelectionContext ctx = new ServerSelectionContext(queryOptions,
_config);
+ if (_adaptiveServerSelector != null && _priorityPoolInstanceSelector !=
null) {
+ if (ctx.isUseFixedReplica()) {
+ // Fixed-replica routing configured on the broker or table disables
adaptive routing in InstanceSelectorFactory.
+ // This fixed replica config can still be set per query, in which case
we'll prefer that over adaptive routing.
+ return selectServers(segments, requestId, segmentStates, null, ctx);
+ }
+ if (QueryOptionsUtils.getNumReplicaGroupsToQuery(ctx.getQueryOptions())
!= null) {
+ // This option intentionally fans segments across replica groups, so
preserve the non-adaptive behavior.
+ // This contradicts the behaviour specified by StrictReplicaGroup, so
we should eventually reject / ignore
+ // this combination.
+ return selectServers(segments, requestId, segmentStates, null, ctx);
+ }
+ }
+ return selectWithContext(segments, requestId, segmentStates, ctx);
+ }
}
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
index 269243cb5b6..dfde3e9c950 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
@@ -45,7 +45,6 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.AccessOption;
import org.apache.helix.BaseDataAccessor;
import org.apache.helix.HelixConstants.ChangeType;
@@ -815,6 +814,8 @@ public abstract class BaseBrokerRoutingManager implements
RoutingManager, Cluste
AdaptiveServerSelector adaptiveServerSelector =
AdaptiveServerSelectorFactory.getAdaptiveServerSelector(_serverRoutingStatsManager,
_pinotConfig);
+ // For StrictReplicaGroupInstanceSelector tables, pool-level adaptive
routing is used,
+ // preserving the same-replica-group guarantee while benefiting from
adaptive server selection.
InstanceSelector instanceSelector =
InstanceSelectorFactory.getInstanceSelector(tableConfig,
_propertyStore, _brokerMetrics,
adaptiveServerSelector, _pinotConfig,
_routableServerInstanceMap.keySet(), _enabledServerInstanceMap,
@@ -1551,8 +1552,7 @@ public abstract class BaseBrokerRoutingManager implements
RoutingManager, Cluste
selectionResult.setNumPrunedSegments(numPrunedSegments);
return selectionResult;
} else {
- return new InstanceSelector.SelectionResult(Pair.of(Map.of(),
Map.of()),
- List.of(), numPrunedSegments);
+ return InstanceSelector.SelectionResult.empty(numPrunedSegments);
}
}
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java
index 796ee992b1d..a7fe5b32e10 100644
---
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java
@@ -78,6 +78,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@@ -252,6 +253,25 @@ public class InstanceSelectorTest {
Set.of()) instanceof ReplicaGroupInstanceSelector);
}
+ @Test
+ public void
testFactoryDisablesAdaptiveRoutingForLegacyUpsertReplicaGroupSelector() {
+ TableConfig tableConfig = mock(TableConfig.class);
+ RoutingConfig routingConfig = mock(RoutingConfig.class);
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ when(tableConfig.getTableName()).thenReturn("legacyUpsert_REALTIME");
+ when(tableConfig.getRoutingConfig()).thenReturn(routingConfig);
+ when(tableConfig.isUpsertEnabled()).thenReturn(true);
+
when(routingConfig.getInstanceSelectorType()).thenReturn(REPLICA_GROUP_INSTANCE_SELECTOR_TYPE);
+
+ ReplicaGroupInstanceSelector instanceSelector =
+ (ReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig, _propertyStore,
+ _brokerMetrics, hybridSelector, new PinotConfiguration(),
Set.of(), Map.of(),
+ new IdealState("legacyUpsert_REALTIME"), new
ExternalView("legacyUpsert_REALTIME"), Set.of());
+
+ assertNull(instanceSelector._adaptiveServerSelector);
+ assertNull(instanceSelector._priorityPoolInstanceSelector);
+ }
+
@Test
public void testInstanceSelector() {
String offlineTableName = "testTable_OFFLINE";
@@ -1863,76 +1883,6 @@ public class InstanceSelectorTest {
assertTrue(selectionResult.getUnavailableSegments().isEmpty());
}
- @Test
- public void testReplicaGroupAdaptiveServerSelector() {
- // Arrange
- ZkHelixPropertyStore<ZNRecord> propertyStore =
mock(ZkHelixPropertyStore.class);
- BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
- HybridSelector hybridSelector = mock(HybridSelector.class);
- ReplicaGroupInstanceSelector instanceSelector = new
ReplicaGroupInstanceSelector();
-
- // Define instances and segments
- String instance0 = "instance0";
- String instance1 = "instance1";
- String instance2 = "instance2";
- String instance3 = "instance3";
- String instance4 = "instance4";
- String segment0 = "segment0";
- String segment1 = "segment1";
- String segment2 = "segment2";
- List<String> segments = Arrays.asList(segment0, segment1, segment2);
-
- // Define candidates for each segment
- Map<String, List<SegmentInstanceCandidate>> instanceCandidatesMap = new
HashMap<>();
- // segment0 -> instance0, instance1
- instanceCandidatesMap.put(segment0,
- Arrays.asList(new SegmentInstanceCandidate(instance0, true), new
SegmentInstanceCandidate(instance1, true)));
- // segment1 -> instance2, instance3
- instanceCandidatesMap.put(segment1,
- Arrays.asList(new SegmentInstanceCandidate(instance2, true), new
SegmentInstanceCandidate(instance3, true)));
- // segment2 -> instance3, instance4 // instance4 is not in the hybrid
selector's server ranking
- instanceCandidatesMap.put(segment2,
- Arrays.asList(new SegmentInstanceCandidate(instance4, true), new
SegmentInstanceCandidate(instance3, true)));
-
- IdealState idealState = createIdealState(
- Map.of(segment0, List.of(Pair.of(instance0, ONLINE),
Pair.of(instance1, ONLINE)), segment1,
- List.of(Pair.of(instance2, ONLINE), Pair.of(instance3, ONLINE)),
segment2,
- List.of(Pair.of(instance3, ONLINE), Pair.of(instance4, ONLINE))));
-
- ExternalView externalView = createExternalView(
- Map.of(segment0, List.of(Pair.of(instance0, ONLINE),
Pair.of(instance1, ONLINE)), segment1,
- List.of(Pair.of(instance2, ONLINE), Pair.of(instance3, ONLINE)),
segment2,
- List.of(Pair.of(instance3, ONLINE), Pair.of(instance4, ONLINE))));
-
- instanceSelector.init(_tableConfig, propertyStore, brokerMetrics,
hybridSelector, Clock.systemUTC(),
- INSTANCE_SELECTOR_CONFIG, Set.of(instance0, instance1, instance2,
instance3, instance4), EMPTY_SERVER_MAP,
- idealState, externalView, new HashSet<>(segments));
-
- // Define the segment states
- SegmentStates segmentStates = new SegmentStates(instanceCandidatesMap, new
HashSet<>(segments), null);
-
- // Define server rankings
- List<Pair<String, Double>> serverRanks = Arrays.asList(
- new ImmutablePair<>(instance3, 1.0),
- new ImmutablePair<>(instance2, 2.0),
- new ImmutablePair<>(instance1, 3.0),
- new ImmutablePair<>(instance0, 4.0)
- );
-
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(serverRanks);
-
- // Act
- Pair<Map<String, String>, Map<String, String>> selectedResult =
- instanceSelector.select(segments, 0, segmentStates, null);
-
- // Assert
- Map<String, String> expectedSelection = new HashMap<>();
- expectedSelection.put(segment0, instance1);
- expectedSelection.put(segment1, instance3);
- expectedSelection.put(segment2, instance4);
-
- assertEquals(selectedResult.getLeft(), expectedSelection);
- }
-
// Replica health metrics
//
// The scenarios below all use the same three instances and assert on the
TableReplicaHealth the
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupSelectorTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupSelectorTest.java
new file mode 100644
index 00000000000..1442ef17672
--- /dev/null
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/ReplicaGroupSelectorTest.java
@@ -0,0 +1,1063 @@
+/**
+ * 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.pinot.broker.routing.instanceselector;
+
+import java.time.Clock;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.helix.model.ExternalView;
+import org.apache.helix.model.IdealState;
+import org.apache.helix.store.zk.ZkHelixPropertyStore;
+import org.apache.helix.zookeeper.datamodel.ZNRecord;
+import org.apache.pinot.broker.routing.adaptiveserverselector.HybridSelector;
+import org.apache.pinot.common.metrics.BrokerMeter;
+import org.apache.pinot.common.metrics.BrokerMetrics;
+import org.apache.pinot.common.request.BrokerRequest;
+import org.apache.pinot.common.request.PinotQuery;
+import org.apache.pinot.core.transport.ServerInstance;
+import org.apache.pinot.spi.config.table.RoutingConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static
org.apache.pinot.spi.config.table.RoutingConfig.REPLICA_GROUP_INSTANCE_SELECTOR_TYPE;
+import static
org.apache.pinot.spi.config.table.RoutingConfig.STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE;
+import static
org.apache.pinot.spi.utils.CommonConstants.Broker.FALLBACK_POOL_ID;
+import static
org.apache.pinot.spi.utils.CommonConstants.Helix.StateModel.SegmentStateModel.OFFLINE;
+import static
org.apache.pinot.spi.utils.CommonConstants.Helix.StateModel.SegmentStateModel.ONLINE;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Tests for [ReplicaGroupInstanceSelector] and
[StrictReplicaGroupInstanceSelector], including adaptive server
+/// selection for strict replica groups.
+@SuppressWarnings("unchecked")
+public class ReplicaGroupSelectorTest {
+ private AutoCloseable _mocks;
+
+ @Mock
+ private TableConfig _tableConfig;
+
+ private static final String TABLE_NAME = "testTable_OFFLINE";
+ private static final Map<String, ServerInstance> EMPTY_SERVER_MAP = Map.of();
+ private static final InstanceSelectorConfig INSTANCE_SELECTOR_CONFIG = new
InstanceSelectorConfig(false, 300, false);
+ private static final List<String> SEGMENTS =
+ Arrays.asList("segment0", "segment1", "segment2", "segment3",
"segment4", "segment5", "segment6", "segment7",
+ "segment8", "segment9", "segment10", "segment11");
+
+ @BeforeMethod
+ public void setUp() {
+ _mocks = MockitoAnnotations.openMocks(this);
+ when(_tableConfig.getTableName()).thenReturn(TABLE_NAME);
+ }
+
+ @AfterMethod
+ public void tearDown()
+ throws Exception {
+ _mocks.close();
+ }
+
+ // --- Shared helpers ---
+
+ static IdealState createIdealState(Map<String, List<Pair<String, String>>>
segmentState) {
+ IdealState idealState = new IdealState(TABLE_NAME);
+ Map<String, Map<String, String>> idealStateSegmentAssignment =
idealState.getRecord().getMapFields();
+ for (Map.Entry<String, List<Pair<String, String>>> entry :
segmentState.entrySet()) {
+ Map<String, String> instanceStateMap = new TreeMap<>();
+ for (Pair<String, String> instanceState : entry.getValue()) {
+ instanceStateMap.put(instanceState.getLeft(),
instanceState.getRight());
+ }
+ idealStateSegmentAssignment.put(entry.getKey(), instanceStateMap);
+ }
+ return idealState;
+ }
+
+ static ExternalView createExternalView(Map<String, List<Pair<String,
String>>> segmentState) {
+ ExternalView externalView = new ExternalView(TABLE_NAME);
+ Map<String, Map<String, String>> externalViewSegmentAssignment =
externalView.getRecord().getMapFields();
+ for (Map.Entry<String, List<Pair<String, String>>> entry :
segmentState.entrySet()) {
+ Map<String, String> instanceStateMap = new TreeMap<>();
+ for (Pair<String, String> instanceState : entry.getValue()) {
+ instanceStateMap.put(instanceState.getLeft(),
instanceState.getRight());
+ }
+ externalViewSegmentAssignment.put(entry.getKey(), instanceStateMap);
+ }
+ return externalView;
+ }
+
+ // --- ReplicaGroupInstanceSelector: numReplicaGroupsToQuery tests ---
+
+ @Test
+ public void testReplicaGroupInstanceSelectorNumReplicaGroupsToQuery() {
+ String offlineTableName = "testTable_OFFLINE";
+ ZkHelixPropertyStore<ZNRecord> propertyStore =
mock(ZkHelixPropertyStore.class);
+ BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
+ BrokerRequest brokerRequest = mock(BrokerRequest.class);
+ PinotQuery pinotQuery = mock(PinotQuery.class);
+ Map<String, String> queryOptions = new HashMap<>();
+ // numReplicas = 3, fanning the query to 2 replica groups
+ queryOptions.put("numReplicaGroupsToQuery", "2");
+ when(brokerRequest.getPinotQuery()).thenReturn(pinotQuery);
+ when(pinotQuery.getQueryOptions()).thenReturn(queryOptions);
+
+ ReplicaGroupInstanceSelector replicaGroupInstanceSelector = new
ReplicaGroupInstanceSelector();
+
+ Set<String> enabledInstances = new HashSet<>();
+ IdealState idealState = new IdealState(offlineTableName);
+ Map<String, Map<String, String>> idealStateSegmentAssignment =
idealState.getRecord().getMapFields();
+ ExternalView externalView = new ExternalView(offlineTableName);
+ Map<String, Map<String, String>> externalViewSegmentAssignment =
externalView.getRecord().getMapFields();
+ Set<String> onlineSegments = new HashSet<>();
+
+ // 12 online segments with each segment having all 3 instances as online
+ // replicas are 3
+ String instance0 = "instance0";
+ String instance1 = "instance1";
+ String instance2 = "instance2";
+ enabledInstances.add(instance0);
+ enabledInstances.add(instance1);
+ enabledInstances.add(instance2);
+
+ Map<String, String> idealStateInstanceStateMap0 = new TreeMap<>();
+ Map<String, String> externalViewInstanceStateMap0 = new TreeMap<>();
+
+ for (String instance : enabledInstances) {
+ idealStateInstanceStateMap0.put(instance, ONLINE);
+ externalViewInstanceStateMap0.put(instance, ONLINE);
+ }
+
+ List<String> segments = SEGMENTS;
+ // add all segments to both idealStateSegmentAssignment and
externalViewSegmentAssignment maps and also to online
+ // segments
+ for (String segment : segments) {
+ idealStateSegmentAssignment.put(segment, idealStateInstanceStateMap0);
+ externalViewSegmentAssignment.put(segment,
externalViewInstanceStateMap0);
+ onlineSegments.add(segment);
+ }
+
+ replicaGroupInstanceSelector.init(_tableConfig, propertyStore,
brokerMetrics, null, Clock.systemUTC(),
+ INSTANCE_SELECTOR_CONFIG, enabledInstances, EMPTY_SERVER_MAP,
idealState, externalView, onlineSegments);
+
+ Map<String, String> expectedReplicaGroupInstanceSelectorResult = new
HashMap<>();
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(0), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(1), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(2), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(3), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(4), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(5), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(6), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(7), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(8), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(9), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(10),
instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(11),
instance1);
+ InstanceSelector.SelectionResult selectionResult =
replicaGroupInstanceSelector.select(brokerRequest, segments, 0);
+ assertEquals(selectionResult.getSegmentToInstanceMap(),
expectedReplicaGroupInstanceSelectorResult);
+ assertTrue(selectionResult.getUnavailableSegments().isEmpty());
+ }
+
+ @Test
+ public void
testReplicaGroupInstanceSelectorNumReplicaGroupsToQueryGreaterThanReplicas() {
+ String offlineTableName = "testTable_OFFLINE";
+ ZkHelixPropertyStore<ZNRecord> propertyStore =
mock(ZkHelixPropertyStore.class);
+ BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
+ BrokerRequest brokerRequest = mock(BrokerRequest.class);
+ PinotQuery pinotQuery = mock(PinotQuery.class);
+ Map<String, String> queryOptions = new HashMap<>();
+ queryOptions.put("numReplicaGroupsToQuery", "4");
+
+ when(brokerRequest.getPinotQuery()).thenReturn(pinotQuery);
+ when(pinotQuery.getQueryOptions()).thenReturn(queryOptions);
+
+ ReplicaGroupInstanceSelector replicaGroupInstanceSelector = new
ReplicaGroupInstanceSelector();
+
+ Set<String> enabledInstances = new HashSet<>();
+ IdealState idealState = new IdealState(offlineTableName);
+ Map<String, Map<String, String>> idealStateSegmentAssignment =
idealState.getRecord().getMapFields();
+ ExternalView externalView = new ExternalView(offlineTableName);
+ Map<String, Map<String, String>> externalViewSegmentAssignment =
externalView.getRecord().getMapFields();
+ Set<String> onlineSegments = new HashSet<>();
+
+ // 12 online segments with each segment having all 3 instances as online
+ // replicas are 3
+ String instance0 = "instance0";
+ String instance1 = "instance1";
+ String instance2 = "instance2";
+ enabledInstances.add(instance0);
+ enabledInstances.add(instance1);
+ enabledInstances.add(instance2);
+
+ List<String> segments = SEGMENTS;
+
+ Map<String, String> idealStateInstanceStateMap0 = new TreeMap<>();
+ Map<String, String> externalViewInstanceStateMap0 = new TreeMap<>();
+
+ for (String instance : enabledInstances) {
+ idealStateInstanceStateMap0.put(instance, ONLINE);
+ externalViewInstanceStateMap0.put(instance, ONLINE);
+ }
+
+ // add all segments to both idealStateSegmentAssignment and
externalViewSegmentAssignment maps and also to online
+ // segments
+ for (String segment : segments) {
+ idealStateSegmentAssignment.put(segment, idealStateInstanceStateMap0);
+ externalViewSegmentAssignment.put(segment,
externalViewInstanceStateMap0);
+ onlineSegments.add(segment);
+ }
+
+ replicaGroupInstanceSelector.init(_tableConfig, propertyStore,
brokerMetrics, null, Clock.systemUTC(),
+ INSTANCE_SELECTOR_CONFIG, enabledInstances, EMPTY_SERVER_MAP,
idealState, externalView, onlineSegments);
+
+ Map<String, String> expectedReplicaGroupInstanceSelectorResult = new
HashMap<>();
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(0), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(1), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(2), instance2);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(3), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(4), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(5), instance2);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(6), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(7), instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(8), instance2);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(9), instance0);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(10),
instance1);
+ expectedReplicaGroupInstanceSelectorResult.put(segments.get(11),
instance2);
+ InstanceSelector.SelectionResult selectionResult =
replicaGroupInstanceSelector.select(brokerRequest, segments, 0);
+ assertEquals(selectionResult.getSegmentToInstanceMap(),
expectedReplicaGroupInstanceSelectorResult);
+ assertTrue(selectionResult.getUnavailableSegments().isEmpty());
+ }
+
+ @Test
+ public void testReplicaGroupInstanceSelectorNumReplicaGroupsNotSet() {
+ String offlineTableName = "testTable_OFFLINE";
+ ZkHelixPropertyStore<ZNRecord> propertyStore =
mock(ZkHelixPropertyStore.class);
+ BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
+ BrokerRequest brokerRequest = mock(BrokerRequest.class);
+ PinotQuery pinotQuery = mock(PinotQuery.class);
+ Map<String, String> queryOptions = new HashMap<>();
+
+ when(brokerRequest.getPinotQuery()).thenReturn(pinotQuery);
+ when(pinotQuery.getQueryOptions()).thenReturn(queryOptions);
+
+ ReplicaGroupInstanceSelector replicaGroupInstanceSelector = new
ReplicaGroupInstanceSelector();
+
+ Set<String> enabledInstances = new HashSet<>();
+ IdealState idealState = new IdealState(offlineTableName);
+ Map<String, Map<String, String>> idealStateSegmentAssignment =
idealState.getRecord().getMapFields();
+ ExternalView externalView = new ExternalView(offlineTableName);
+ Map<String, Map<String, String>> externalViewSegmentAssignment =
externalView.getRecord().getMapFields();
+ Set<String> onlineSegments = new HashSet<>();
+
+ // 12 online segments with each segment having all 3 instances as online
+ // replicas are 3
+ String instance0 = "instance0";
+ String instance1 = "instance1";
+ String instance2 = "instance2";
+ enabledInstances.add(instance0);
+ enabledInstances.add(instance1);
+ enabledInstances.add(instance2);
+
+ List<String> segments = SEGMENTS;
+
+ Map<String, String> idealStateInstanceStateMap0 = new TreeMap<>();
+ Map<String, String> externalViewInstanceStateMap0 = new TreeMap<>();
+
+ for (String instance : enabledInstances) {
+ idealStateInstanceStateMap0.put(instance, ONLINE);
+ externalViewInstanceStateMap0.put(instance, ONLINE);
+ }
+
+ // add all segments to both idealStateSegmentAssignment and
externalViewSegmentAssignment maps and also to online
+ // segments
+ for (String segment : segments) {
+ idealStateSegmentAssignment.put(segment, idealStateInstanceStateMap0);
+ externalViewSegmentAssignment.put(segment,
externalViewInstanceStateMap0);
+ onlineSegments.add(segment);
+ }
+
+ replicaGroupInstanceSelector.init(_tableConfig, propertyStore,
brokerMetrics, null, Clock.systemUTC(),
+ INSTANCE_SELECTOR_CONFIG, enabledInstances, EMPTY_SERVER_MAP,
idealState, externalView, onlineSegments);
+ // since numReplicaGroupsToQuery is not set, first query should go to
first replica group,
+ // 2nd query should go to next replica group
+
+ Map<String, String> expectedReplicaGroupInstanceSelectorResult = new
HashMap<>();
+ for (String segment : segments) {
+ expectedReplicaGroupInstanceSelectorResult.put(segment, instance0);
+ }
+ InstanceSelector.SelectionResult selectionResult =
replicaGroupInstanceSelector.select(brokerRequest, segments, 0);
+ assertEquals(selectionResult.getSegmentToInstanceMap(),
expectedReplicaGroupInstanceSelectorResult);
+
+ for (String segment : segments) {
+ expectedReplicaGroupInstanceSelectorResult.put(segment, instance1);
+ }
+ selectionResult = replicaGroupInstanceSelector.select(brokerRequest,
segments, 1);
+ assertEquals(selectionResult.getSegmentToInstanceMap(),
expectedReplicaGroupInstanceSelectorResult);
+ }
+
+ // --- Adaptive server selection tests ---
+
+ // Shared topology for AR tests: 3 segments across 5 instances in two pools
/ replica groups.
+ // segment2 intentionally has AR_P0_RG0_SERVER_E (unranked) so AR falls back
to round-robin for that segment.
+ private static final String AR_P0_RG0_SERVER_A = "ar_p0_rg0_server_a";
+ private static final String AR_P1_RG1_SERVER_B = "ar_p1_rg1_server_b";
+ private static final String AR_P0_RG0_SERVER_C = "ar_p0_rg0_server_c";
+ private static final String AR_P1_RG1_SERVER_D = "ar_p1_rg1_server_d";
+ private static final String AR_P0_RG0_SERVER_E = "ar_p0_rg0_server_e";
+ private static final String AR_SEGMENT0 = "segment0";
+ private static final String AR_SEGMENT1 = "segment1";
+ private static final String AR_SEGMENT2 = "segment2";
+ private static final List<String> AR_SEGMENTS =
+ Arrays.asList(AR_SEGMENT0, AR_SEGMENT1, AR_SEGMENT2);
+ // Rankings: D best → C → B → A worst; E absent (triggers AR fallback)
+ private static final List<Pair<String, Double>> AR_SERVER_RANKS =
Arrays.asList(
+ new ImmutablePair<>(AR_P1_RG1_SERVER_D, 1.0),
+ new ImmutablePair<>(AR_P0_RG0_SERVER_C, 2.0),
+ new ImmutablePair<>(AR_P1_RG1_SERVER_B, 3.0),
+ new ImmutablePair<>(AR_P0_RG0_SERVER_A, 4.0)
+ );
+
+ private SegmentStates buildArSegmentStates() {
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ // segment0 → pool 0 / replica group 0 server A, pool 1 / replica group 1
server B
+ candidatesMap.put(AR_SEGMENT0, Arrays.asList(
+ new SegmentInstanceCandidate(AR_P0_RG0_SERVER_A, true, 0, 0),
+ new SegmentInstanceCandidate(AR_P1_RG1_SERVER_B, true, 1, 1)));
+ // segment1 → pool 0 / replica group 0 server C, pool 1 / replica group 1
server D
+ candidatesMap.put(AR_SEGMENT1, Arrays.asList(
+ new SegmentInstanceCandidate(AR_P0_RG0_SERVER_C, true, 0, 0),
+ new SegmentInstanceCandidate(AR_P1_RG1_SERVER_D, true, 1, 1)));
+ // segment2 → pool 0 / replica group 0 server E (unranked), pool 1 /
replica group 1 server D
+ candidatesMap.put(AR_SEGMENT2, Arrays.asList(
+ new SegmentInstanceCandidate(AR_P0_RG0_SERVER_E, true, 0, 0),
+ new SegmentInstanceCandidate(AR_P1_RG1_SERVER_D, true, 1, 1)));
+ return new SegmentStates(candidatesMap, new HashSet<>(AR_SEGMENTS), null);
+ }
+
+ private ReplicaGroupInstanceSelector buildArSelector(String selectorType,
HybridSelector hybridSelector) {
+ return buildArSelector(selectorType, hybridSelector, new
PinotConfiguration(Map.of()));
+ }
+
+ private ReplicaGroupInstanceSelector buildArSelector(String selectorType,
HybridSelector hybridSelector,
+ PinotConfiguration brokerConfig) {
+ RoutingConfig routingConfig = new RoutingConfig(null, null, selectorType,
false);
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("testTable")
+ .setRoutingConfig(routingConfig).build();
+ IdealState idealState = createIdealState(Map.of(
+ AR_SEGMENT0, List.of(Pair.of(AR_P0_RG0_SERVER_A, ONLINE),
Pair.of(AR_P1_RG1_SERVER_B, ONLINE)),
+ AR_SEGMENT1, List.of(Pair.of(AR_P0_RG0_SERVER_C, ONLINE),
Pair.of(AR_P1_RG1_SERVER_D, ONLINE)),
+ AR_SEGMENT2, List.of(Pair.of(AR_P1_RG1_SERVER_D, ONLINE),
Pair.of(AR_P0_RG0_SERVER_E, ONLINE))));
+ ExternalView externalView = createExternalView(Map.of(
+ AR_SEGMENT0, List.of(Pair.of(AR_P0_RG0_SERVER_A, ONLINE),
Pair.of(AR_P1_RG1_SERVER_B, ONLINE)),
+ AR_SEGMENT1, List.of(Pair.of(AR_P0_RG0_SERVER_C, ONLINE),
Pair.of(AR_P1_RG1_SERVER_D, ONLINE)),
+ AR_SEGMENT2, List.of(Pair.of(AR_P1_RG1_SERVER_D, ONLINE),
Pair.of(AR_P0_RG0_SERVER_E, ONLINE))));
+ ServerInstance serverA = mock(ServerInstance.class);
+ when(serverA.getPool()).thenReturn(0);
+ ServerInstance serverB = mock(ServerInstance.class);
+ when(serverB.getPool()).thenReturn(1);
+ ServerInstance serverC = mock(ServerInstance.class);
+ when(serverC.getPool()).thenReturn(0);
+ ServerInstance serverD = mock(ServerInstance.class);
+ when(serverD.getPool()).thenReturn(1);
+ ServerInstance serverE = mock(ServerInstance.class);
+ when(serverE.getPool()).thenReturn(0);
+ Map<String, ServerInstance> serverMap = Map.of(
+ AR_P0_RG0_SERVER_A, serverA,
+ AR_P1_RG1_SERVER_B, serverB,
+ AR_P0_RG0_SERVER_C, serverC,
+ AR_P1_RG1_SERVER_D, serverD,
+ AR_P0_RG0_SERVER_E, serverE);
+ return (ReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig,
+ mock(ZkHelixPropertyStore.class), mock(BrokerMetrics.class),
hybridSelector,
+ brokerConfig,
+ Set.of(AR_P0_RG0_SERVER_A, AR_P1_RG1_SERVER_B, AR_P0_RG0_SERVER_C,
AR_P1_RG1_SERVER_D, AR_P0_RG0_SERVER_E),
+ serverMap, idealState, externalView, new HashSet<>(AR_SEGMENTS));
+ }
+
+ @Test
+ public void testReplicaGroupAdaptiveServerSelector() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ ReplicaGroupInstanceSelector instanceSelector =
+ buildArSelector(REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, hybridSelector);
+
+ assertTrue(instanceSelector instanceof ReplicaGroupInstanceSelector);
+ assertFalse(instanceSelector instanceof
StrictReplicaGroupInstanceSelector);
+ assertNotNull(instanceSelector._adaptiveServerSelector);
+ assertNotNull(instanceSelector._priorityPoolInstanceSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(AR_SERVER_RANKS);
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(AR_SEGMENTS, 0, buildArSegmentStates(), null);
+
+ // AR prefers the better-ranked server when all candidates are ranked:
+ // segment0: B (rank 3) over A (rank 4)
+ // segment1: D (rank 1) over C (rank 2)
+ // segment2: E unranked → AR falls back to round-robin → index 0 = E
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ AR_SEGMENT0, AR_P1_RG1_SERVER_B,
+ AR_SEGMENT1, AR_P1_RG1_SERVER_D,
+ AR_SEGMENT2, AR_P0_RG0_SERVER_E));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveDisabledByFeatureFlag() {
+ // When the feature flag is disabled, the factory nulls out the adaptive
selector so the selector falls back to the
+ // strict selector's non-adaptive, round-robin-by-replica-index path.
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
+ PinotConfiguration brokerConfig = new PinotConfiguration(Map.of(
+
CommonConstants.Broker.AdaptiveServerSelector.CONFIG_OF_STRICT_REPLICA_GROUP_ENABLED,
"false"));
+ StrictReplicaGroupInstanceSelector instanceSelector =
+ buildStrictReplicaGroupArSelector(hybridSelector, brokerMetrics,
brokerConfig);
+
+ assertNull(instanceSelector._adaptiveServerSelector);
+ assertNull(instanceSelector._priorityPoolInstanceSelector);
+
+ BaseInstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+
+ verify(hybridSelector, never()).fetchServerRankingsWithScores(any());
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT1, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+
+ result = instanceSelector.select(STRICT_SEGMENTS, 1,
buildStrictReplicaGroupSegmentStates(), null);
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+ }
+
+ @Test
+ public void testStrictReplicaGroupFactoryEnablesAdaptiveRouting() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ ReplicaGroupInstanceSelector instanceSelector =
+ buildArSelector(STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE,
hybridSelector);
+
+ assertTrue(instanceSelector instanceof StrictReplicaGroupInstanceSelector);
+ assertNotNull(instanceSelector._adaptiveServerSelector);
+ assertNotNull(instanceSelector._priorityPoolInstanceSelector);
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptiveDisabledByFixedReplicaBrokerConfig() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ PinotConfiguration brokerConfig = new PinotConfiguration(Map.of(
+ CommonConstants.Broker.CONFIG_OF_USE_FIXED_REPLICA, "true"));
+ StrictReplicaGroupInstanceSelector instanceSelector =
+ buildStrictReplicaGroupArSelector(hybridSelector,
mock(BrokerMetrics.class), brokerConfig);
+
+ assertNull(instanceSelector._adaptiveServerSelector);
+ assertNull(instanceSelector._priorityPoolInstanceSelector);
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+ verify(hybridSelector, never()).fetchServerRankingsWithScores(any());
+ assertEquals(result.segmentToInstanceMap().get(STRICT_SEGMENT0),
+ result.segmentToInstanceMap().get(STRICT_SEGMENT1));
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptiveDisabledByFixedReplicaTableConfig() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector,
+ mock(BrokerMetrics.class), new PinotConfiguration(Map.of()), true);
+
+ assertNull(instanceSelector._adaptiveServerSelector);
+ assertNull(instanceSelector._priorityPoolInstanceSelector);
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+ verify(hybridSelector, never()).fetchServerRankingsWithScores(any());
+ assertEquals(result.segmentToInstanceMap().get(STRICT_SEGMENT0),
+ result.segmentToInstanceMap().get(STRICT_SEGMENT1));
+ }
+
+ // --- Strict replica group adaptive routing tests ---
+
+ // Topology: 2 replica groups, 3 segments
+ // Replica group 0: server_a (segment0, segment1), server_b (segment2)
+ // Replica group 1: server_c (segment0, segment1), server_d (segment2)
+ // If a strict-fixture server name omits `P#`, its pool is FALLBACK_POOL_ID.
+ private static final String STRICT_RG0_SERVER_A = "strict_rg0_server_a";
+ private static final String STRICT_RG0_SERVER_B = "strict_rg0_server_b";
+ private static final String STRICT_RG1_SERVER_C = "strict_rg1_server_c";
+ private static final String STRICT_RG1_SERVER_D = "strict_rg1_server_d";
+ private static final String STRICT_SEGMENT0 = "seg0";
+ private static final String STRICT_SEGMENT1 = "seg1";
+ private static final String STRICT_SEGMENT2 = "seg2";
+ private static final List<String> STRICT_SEGMENTS =
Arrays.asList(STRICT_SEGMENT0, STRICT_SEGMENT1, STRICT_SEGMENT2);
+
+ private List<SegmentInstanceCandidate>
buildStrictReplicaGroupCandidates(String replicaGroup0Instance,
+ String replicaGroup1Instance) {
+ return buildStrictReplicaGroupCandidates(replicaGroup0Instance,
replicaGroup1Instance, FALLBACK_POOL_ID,
+ FALLBACK_POOL_ID);
+ }
+
+ private List<SegmentInstanceCandidate>
buildStrictReplicaGroupCandidates(String replicaGroup0Instance,
+ String replicaGroup1Instance, int replicaGroup0Pool, int
replicaGroup1Pool) {
+ return Arrays.asList(
+ new SegmentInstanceCandidate(replicaGroup0Instance, true,
replicaGroup0Pool, 0),
+ new SegmentInstanceCandidate(replicaGroup1Instance, true,
replicaGroup1Pool, 1));
+ }
+
+ private SegmentStates buildStrictReplicaGroupSegmentStates() {
+ return buildStrictReplicaGroupSegmentStates(FALLBACK_POOL_ID,
FALLBACK_POOL_ID);
+ }
+
+ private SegmentStates buildStrictReplicaGroupSegmentStates(int
replicaGroup0Pool, int replicaGroup1Pool) {
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ // All strict-fixture servers intentionally share FALLBACK_POOL_ID so
these tests fail if strict routing regresses
+ // back to grouping or filtering by pool instead of idealStateReplicaId.
+ candidatesMap.put(STRICT_SEGMENT0,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_A,
STRICT_RG1_SERVER_C, replicaGroup0Pool,
+ replicaGroup1Pool));
+ candidatesMap.put(STRICT_SEGMENT1,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_A,
STRICT_RG1_SERVER_C, replicaGroup0Pool,
+ replicaGroup1Pool));
+ candidatesMap.put(STRICT_SEGMENT2,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_B,
STRICT_RG1_SERVER_D, replicaGroup0Pool,
+ replicaGroup1Pool));
+ return new SegmentStates(candidatesMap, new HashSet<>(STRICT_SEGMENTS),
null);
+ }
+
+ private StrictReplicaGroupInstanceSelector
buildStrictReplicaGroupArSelector(HybridSelector hybridSelector) {
+ return buildStrictReplicaGroupArSelector(hybridSelector,
mock(BrokerMetrics.class));
+ }
+
+ private StrictReplicaGroupInstanceSelector
buildStrictReplicaGroupArSelector(HybridSelector hybridSelector,
+ BrokerMetrics brokerMetrics) {
+ return buildStrictReplicaGroupArSelector(hybridSelector, brokerMetrics,
+ new PinotConfiguration(Map.of()));
+ }
+
+ private StrictReplicaGroupInstanceSelector
buildStrictReplicaGroupArSelector(HybridSelector hybridSelector,
+ BrokerMetrics brokerMetrics, PinotConfiguration brokerConfig) {
+ return buildStrictReplicaGroupArSelector(hybridSelector, brokerMetrics,
brokerConfig, null);
+ }
+
+ private StrictReplicaGroupInstanceSelector
buildStrictReplicaGroupArSelector(HybridSelector hybridSelector,
+ BrokerMetrics brokerMetrics, PinotConfiguration brokerConfig, Boolean
useFixedReplica) {
+ RoutingConfig routingConfig = new RoutingConfig(null, null,
STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE,
+ useFixedReplica);
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("testUpsertTable")
+ .setRoutingConfig(routingConfig).build();
+ // Ideal state: mirrors the topology above
+ IdealState idealState = createIdealState(Map.of(
+ STRICT_SEGMENT0, List.of(
+ Pair.of(STRICT_RG0_SERVER_A, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT1, List.of(
+ Pair.of(STRICT_RG0_SERVER_A, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT2, List.of(
+ Pair.of(STRICT_RG0_SERVER_B, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_D, ONLINE))));
+ ExternalView externalView = createExternalView(Map.of(
+ STRICT_SEGMENT0, List.of(
+ Pair.of(STRICT_RG0_SERVER_A, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT1, List.of(
+ Pair.of(STRICT_RG0_SERVER_A, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT2, List.of(
+ Pair.of(STRICT_RG0_SERVER_B, ONLINE),
+ Pair.of(STRICT_RG1_SERVER_D, ONLINE))));
+ // All servers use the fallback pool to prove strict routing keys off
idealStateReplicaId rather than pool.
+ ServerInstance serverA = mock(ServerInstance.class);
+ when(serverA.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverB = mock(ServerInstance.class);
+ when(serverB.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverC = mock(ServerInstance.class);
+ when(serverC.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverD = mock(ServerInstance.class);
+ when(serverD.getPool()).thenReturn(FALLBACK_POOL_ID);
+ Map<String, ServerInstance> serverMap = Map.of(
+ STRICT_RG0_SERVER_A, serverA, STRICT_RG0_SERVER_B, serverB,
+ STRICT_RG1_SERVER_C, serverC, STRICT_RG1_SERVER_D, serverD);
+ return (StrictReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig,
+ mock(ZkHelixPropertyStore.class), brokerMetrics, hybridSelector,
+ brokerConfig,
+ Set.of(STRICT_RG0_SERVER_A, STRICT_RG0_SERVER_B,
+ STRICT_RG1_SERVER_C, STRICT_RG1_SERVER_D),
+ serverMap, idealState, externalView, new HashSet<>(STRICT_SEGMENTS));
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptivePicksHealthiestCandidatePerPartition() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ BrokerMetrics brokerMetrics = mock(BrokerMetrics.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector,
+ brokerMetrics);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 1.0), // rank 0 (best)
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 2.0), // rank 1
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 3.0), // rank 2
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 4.0) // rank 3 (worst)
+ ));
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+
+ // Each partition independently prefers its healthier replica. Segments 0
and 1 share a candidate list, so they
+ // make the same choice.
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+ // Metrics are reported per selected pool.
+ verify(brokerMetrics).addMeteredValue(eq(BrokerMeter.POOL_SEG_QUERIES),
eq(3L),
+ eq(BrokerMetrics.getTagForPreferredPool(null)),
eq(String.valueOf(FALLBACK_POOL_ID)));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptivePicksOtherHealthyCandidates() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 1.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 2.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 3.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 4.0)
+ ));
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT1, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptivePrefersPoolBeforeAdaptiveHealth() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 1.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 2.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 90.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 100.0)
+ ));
+
+ Map<String, String> queryOptions = Map.of(
+ CommonConstants.Broker.Request.QueryOptionKey.ORDERED_PREFERRED_POOLS,
"0");
+ InstanceSelector.InstanceMapping result =
instanceSelector.select(STRICT_SEGMENTS, 0,
+ buildStrictReplicaGroupSegmentStates(0, 1), queryOptions);
+
+ // Preferred pools are the primary selection criterion, even when another
pool is healthier.
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT1, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveUsesHealthWithinPreferredPool() {
+ String nonPreferredServerE = "strict_rg2_server_e";
+ String nonPreferredServerF = "strict_rg2_server_f";
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(nonPreferredServerE, 0.0),
+ new ImmutablePair<>(nonPreferredServerF, 0.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 1.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 2.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 20.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 30.0)
+ ));
+
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ List<SegmentInstanceCandidate> partition0Candidates = Arrays.asList(
+ new SegmentInstanceCandidate(STRICT_RG0_SERVER_A, true, 0, 0),
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_C, true, 0, 1),
+ new SegmentInstanceCandidate(nonPreferredServerE, true, 1, 2));
+ candidatesMap.put(STRICT_SEGMENT0, partition0Candidates);
+ candidatesMap.put(STRICT_SEGMENT1, partition0Candidates);
+ candidatesMap.put(STRICT_SEGMENT2, Arrays.asList(
+ new SegmentInstanceCandidate(STRICT_RG0_SERVER_B, true, 0, 0),
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_D, true, 0, 1),
+ new SegmentInstanceCandidate(nonPreferredServerF, true, 1, 2)));
+ SegmentStates segmentStates = new SegmentStates(candidatesMap, new
HashSet<>(STRICT_SEGMENTS), null);
+
+ Map<String, String> queryOptions = Map.of(
+ CommonConstants.Broker.Request.QueryOptionKey.ORDERED_PREFERRED_POOLS,
"0");
+ InstanceSelector.InstanceMapping result =
instanceSelector.select(STRICT_SEGMENTS, 1, segmentStates,
+ queryOptions);
+
+ // The healthiest candidate within pool 0 wins, while the healthier
candidates in pool 1 are ignored.
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptiveEmptyRankingsFallBackToRoundRobin() {
+ // When adaptive ranking returns no servers, fall back to round-robin by
candidate index.
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(List.of());
+
+ // requestId=0 picks candidate index 0.
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), null);
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT1, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+
+ // requestId=1 picks candidate index 1.
+ result = instanceSelector.select(STRICT_SEGMENTS, 1,
buildStrictReplicaGroupSegmentStates(), null);
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveSupportsFixedReplicaQueryOption() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+ Map<String, String> queryOptions = Map.of(
+ CommonConstants.Broker.Request.QueryOptionKey.USE_FIXED_REPLICA,
"true");
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), queryOptions);
+ verify(hybridSelector, never()).fetchServerRankingsWithScores(any());
+ assertEquals(result.segmentToInstanceMap().get(STRICT_SEGMENT0),
+ result.segmentToInstanceMap().get(STRICT_SEGMENT1));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptivePreservesNumReplicaGroupsToQuery()
{
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+ Map<String, String> queryOptions = Map.of(
+
CommonConstants.Broker.Request.QueryOptionKey.NUM_REPLICA_GROUPS_TO_QUERY, "2");
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0,
buildStrictReplicaGroupSegmentStates(), queryOptions);
+
+ verify(hybridSelector, never()).fetchServerRankingsWithScores(any());
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG0_SERVER_A,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveIsConsistentWithinEachPartition() {
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 1.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 2.0),
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 3.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 4.0)
+ ));
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 42,
buildStrictReplicaGroupSegmentStates(), null);
+
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG0_SERVER_B));
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptiveUsesFilteredCandidatesPerPartition() {
+ String partition0Segment0 = "partition0_segment0";
+ String partition0Segment1 = "partition0_segment1";
+ String partition1Segment0 = "partition1_segment0";
+ String partition1Segment1 = "partition1_segment1";
+ List<String> segments = List.of(partition0Segment0, partition0Segment1,
partition1Segment0,
+ partition1Segment1);
+ String partition0Replica0 = "partition0_replica0";
+ String partition0Replica1 = "partition0_replica1";
+ String partition1Replica0 = "partition1_replica0";
+ String partition1Replica1 = "partition1_replica1";
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+
+ RoutingConfig routingConfig = new RoutingConfig(null, null,
STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false);
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("partitionedTable")
+ .setRoutingConfig(routingConfig).build();
+ IdealState idealState = createIdealState(Map.of(
+ partition0Segment0, List.of(Pair.of(partition0Replica0, ONLINE),
Pair.of(partition0Replica1, ONLINE)),
+ partition0Segment1, List.of(Pair.of(partition0Replica0, ONLINE),
Pair.of(partition0Replica1, ONLINE)),
+ partition1Segment0, List.of(Pair.of(partition1Replica0, ONLINE),
Pair.of(partition1Replica1, ONLINE)),
+ partition1Segment1, List.of(Pair.of(partition1Replica0, ONLINE),
Pair.of(partition1Replica1, ONLINE))));
+ // Degrade opposite replica positions in the two partitions. Strict state
construction must remove each degraded
+ // replica from every old segment with the same IdealState assignment.
+ ExternalView externalView = createExternalView(Map.of(
+ partition0Segment0, List.of(Pair.of(partition0Replica0, ONLINE),
Pair.of(partition0Replica1, ONLINE)),
+ partition0Segment1, List.of(Pair.of(partition0Replica0, OFFLINE),
Pair.of(partition0Replica1, ONLINE)),
+ partition1Segment0, List.of(Pair.of(partition1Replica0, ONLINE),
Pair.of(partition1Replica1, ONLINE)),
+ partition1Segment1, List.of(Pair.of(partition1Replica0, ONLINE),
Pair.of(partition1Replica1, OFFLINE))));
+
+ ServerInstance server0 = mock(ServerInstance.class);
+ when(server0.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance server1 = mock(ServerInstance.class);
+ when(server1.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance server2 = mock(ServerInstance.class);
+ when(server2.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance server3 = mock(ServerInstance.class);
+ when(server3.getPool()).thenReturn(FALLBACK_POOL_ID);
+ Map<String, ServerInstance> serverMap = Map.of(partition0Replica0,
server0, partition0Replica1, server1,
+ partition1Replica0, server2, partition1Replica1, server3);
+ StrictReplicaGroupInstanceSelector instanceSelector =
+ (StrictReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig,
+ mock(ZkHelixPropertyStore.class), mock(BrokerMetrics.class),
hybridSelector,
+ new PinotConfiguration(Map.of()),
+ Set.of(partition0Replica0, partition0Replica1, partition1Replica0,
partition1Replica1), serverMap,
+ idealState, externalView, new HashSet<>(segments));
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(List.of(
+ new ImmutablePair<>(partition1Replica0, 1.0), new
ImmutablePair<>(partition0Replica1, 2.0)));
+
+ BrokerRequest brokerRequest = mock(BrokerRequest.class);
+ PinotQuery pinotQuery = mock(PinotQuery.class);
+ when(brokerRequest.getPinotQuery()).thenReturn(pinotQuery);
+ when(pinotQuery.getQueryOptions()).thenReturn(null);
+
+ InstanceSelector.SelectionResult result =
instanceSelector.select(brokerRequest, segments, 0);
+
+ assertEquals(result.getSegmentToInstanceMap(), Map.of(
+ partition0Segment0, partition0Replica1,
+ partition0Segment1, partition0Replica1,
+ partition1Segment0, partition1Replica0,
+ partition1Segment1, partition1Replica0));
+ assertTrue(result.getUnavailableSegments().isEmpty());
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveNewSegmentNotFalselyUnavailable() {
+ // Topology: server_a (replica group 0) hosts seg0 only. seg1 has no
online instance (simulating a
+ // new/unassigned segment with null candidates in segmentStates). Replica
group 0 is chosen (only group).
+ // seg1 should NOT appear in unavailable — it is a new segment, not a
dropped one.
+ String serverA = "new_server_a";
+ String seg0 = "new_seg0";
+ String seg1 = "new_seg1";
+ List<String> segments = Arrays.asList(seg0, seg1);
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+
+ RoutingConfig routingConfig = new RoutingConfig(null, null,
STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false);
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("testNewSegTable")
+ .setRoutingConfig(routingConfig).build();
+ // seg1 intentionally absent from idealState/externalView → null
candidates (new segment)
+ IdealState idealState = createIdealState(Map.of(
+ seg0, List.of(Pair.of(serverA, ONLINE))));
+ ExternalView externalView = createExternalView(Map.of(
+ seg0, List.of(Pair.of(serverA, ONLINE))));
+ ServerInstance serverAInstance = mock(ServerInstance.class);
+ when(serverAInstance.getPool()).thenReturn(FALLBACK_POOL_ID);
+ StrictReplicaGroupInstanceSelector instanceSelector =
+ (StrictReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig,
+ mock(ZkHelixPropertyStore.class), mock(BrokerMetrics.class),
hybridSelector,
+ new PinotConfiguration(Map.of()),
+ Set.of(serverA),
+ Map.of(serverA, serverAInstance),
+ idealState, externalView, new HashSet<>(List.of(seg0)));
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(serverA, 1.0)));
+
+ BrokerRequest brokerRequest = mock(BrokerRequest.class);
+ PinotQuery pinotQuery = mock(PinotQuery.class);
+ when(brokerRequest.getPinotQuery()).thenReturn(pinotQuery);
+ when(pinotQuery.getQueryOptions()).thenReturn(null);
+
+ InstanceSelector.SelectionResult result =
instanceSelector.select(brokerRequest, segments, 0);
+ assertEquals(result.getSegmentToInstanceMap(), Map.of(seg0, serverA));
+ // seg1 has null candidates (not yet in the selector's state) → must NOT
appear as unavailable
+ assertTrue(result.getUnavailableSegments().isEmpty());
+ }
+
+ @Test
+ public void
testStrictReplicaGroupAdaptiveNewSegmentWithCandidateRemainsOptional() {
+ String newSegment = "new_seg_with_candidate";
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 1.0),
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 2.0)
+ ));
+
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ candidatesMap.put(STRICT_SEGMENT0, List.of(
+ new SegmentInstanceCandidate(STRICT_RG0_SERVER_A, true,
FALLBACK_POOL_ID, 0)));
+ candidatesMap.put(newSegment, List.of(
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_D, false,
FALLBACK_POOL_ID, 1)));
+ SegmentStates segmentStates = new SegmentStates(candidatesMap,
+ Set.of(STRICT_RG0_SERVER_A, STRICT_RG1_SERVER_D), Set.of());
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(List.of(STRICT_SEGMENT0, newSegment), 0,
segmentStates, Map.of());
+
+ assertEquals(result.segmentToInstanceMap(), Map.of(STRICT_SEGMENT0,
STRICT_RG0_SERVER_A));
+ assertEquals(result.optionalSegmentToInstanceMap(), Map.of(newSegment,
STRICT_RG1_SERVER_D));
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveOnlyQueryRelevantServersScored() {
+ // The selector has an extra server (server_e) that only hosts seg_extra,
which is not in the query.
+ // server_e should never be scored for a query over STRICT_SEGMENTS.
+ String rg1ServerE = "server_e";
+ String segExtra = "seg_extra";
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+
+ RoutingConfig routingConfig = new RoutingConfig(null, null,
STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE, false);
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("testUpsertTable")
+ .setRoutingConfig(routingConfig).build();
+ // server_e's pool intentionally differs from the queried servers to show
pool is irrelevant.
+ // segExtra places server_d at position 0 and server_e at position 1
(idealStateReplicaId=1).
+ IdealState idealState = createIdealState(Map.of(
+ STRICT_SEGMENT0, List.of(Pair.of(STRICT_RG0_SERVER_A, ONLINE),
Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT1, List.of(Pair.of(STRICT_RG0_SERVER_A, ONLINE),
Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT2, List.of(Pair.of(STRICT_RG0_SERVER_B, ONLINE),
Pair.of(STRICT_RG1_SERVER_D, ONLINE)),
+ segExtra, List.of(Pair.of(STRICT_RG1_SERVER_D, ONLINE),
Pair.of(rg1ServerE, ONLINE))));
+ ExternalView externalView = createExternalView(Map.of(
+ STRICT_SEGMENT0, List.of(Pair.of(STRICT_RG0_SERVER_A, ONLINE),
Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT1, List.of(Pair.of(STRICT_RG0_SERVER_A, ONLINE),
Pair.of(STRICT_RG1_SERVER_C, ONLINE)),
+ STRICT_SEGMENT2, List.of(Pair.of(STRICT_RG0_SERVER_B, ONLINE),
Pair.of(STRICT_RG1_SERVER_D, ONLINE)),
+ segExtra, List.of(Pair.of(STRICT_RG1_SERVER_D, ONLINE),
Pair.of(rg1ServerE, ONLINE))));
+ ServerInstance serverA = mock(ServerInstance.class);
+ when(serverA.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverB = mock(ServerInstance.class);
+ when(serverB.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverC = mock(ServerInstance.class);
+ when(serverC.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverD = mock(ServerInstance.class);
+ when(serverD.getPool()).thenReturn(FALLBACK_POOL_ID);
+ ServerInstance serverEInstance = mock(ServerInstance.class);
+ when(serverEInstance.getPool()).thenReturn(99);
+ Map<String, ServerInstance> serverMap = Map.of(
+ STRICT_RG0_SERVER_A, serverA, STRICT_RG0_SERVER_B, serverB,
+ STRICT_RG1_SERVER_C, serverC, STRICT_RG1_SERVER_D, serverD,
+ rg1ServerE, serverEInstance);
+ Set<String> allSegments = new HashSet<>(STRICT_SEGMENTS);
+ allSegments.add(segExtra);
+ StrictReplicaGroupInstanceSelector instanceSelector =
+ (StrictReplicaGroupInstanceSelector)
InstanceSelectorFactory.getInstanceSelector(tableConfig,
+ mock(ZkHelixPropertyStore.class), mock(BrokerMetrics.class),
hybridSelector,
+ new PinotConfiguration(Map.of()),
+ Set.of(STRICT_RG0_SERVER_A, STRICT_RG0_SERVER_B,
+ STRICT_RG1_SERVER_C, STRICT_RG1_SERVER_D, rg1ServerE),
+ serverMap, idealState, externalView, allSegments);
+
+ // server_e has a much worse mocked score but hosts no query segments, so
it should never be ranked.
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(Arrays.asList(
+ new ImmutablePair<>(STRICT_RG1_SERVER_C, 1.0), // rank 0
+ new ImmutablePair<>(STRICT_RG1_SERVER_D, 2.0), // rank 1
+ new ImmutablePair<>(STRICT_RG0_SERVER_A, 3.0), // rank 2
+ new ImmutablePair<>(STRICT_RG0_SERVER_B, 4.0), // rank 3
+ new ImmutablePair<>(rg1ServerE, 99.0) // rank 4 (not
query-relevant)
+ ));
+
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ candidatesMap.put(STRICT_SEGMENT0,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_A,
STRICT_RG1_SERVER_C));
+ candidatesMap.put(STRICT_SEGMENT1,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_A,
STRICT_RG1_SERVER_C));
+ candidatesMap.put(STRICT_SEGMENT2,
+ buildStrictReplicaGroupCandidates(STRICT_RG0_SERVER_B,
STRICT_RG1_SERVER_D));
+ candidatesMap.put(segExtra, Arrays.asList(
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_D, true,
FALLBACK_POOL_ID, 0),
+ new SegmentInstanceCandidate(rg1ServerE, true, 99, 1)));
+ SegmentStates segmentStates = new SegmentStates(candidatesMap,
allSegments, null);
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, 0, segmentStates, null);
+
+ // Each query partition picks its healthiest candidate; server_e is not
considered because it hosts no query
+ // segments.
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+
+ ArgumentCaptor<List<String>> rankedCandidatesCaptor =
ArgumentCaptor.forClass(List.class);
+
verify(hybridSelector).fetchServerRankingsWithScores(rankedCandidatesCaptor.capture());
+ assertEquals(new HashSet<>(rankedCandidatesCaptor.getValue()),
+ Set.of(STRICT_RG0_SERVER_A, STRICT_RG0_SERVER_B,
+ STRICT_RG1_SERVER_C, STRICT_RG1_SERVER_D));
+ assertEquals(rankedCandidatesCaptor.getValue().size(), 4);
+ }
+
+ @Test
+ public void testStrictReplicaGroupAdaptiveHandlesMinimumRequestId() {
+ String replicaGroup2ServerE = "strict_rg2_server_e";
+ String replicaGroup2ServerF = "strict_rg2_server_f";
+ HybridSelector hybridSelector = mock(HybridSelector.class);
+ StrictReplicaGroupInstanceSelector instanceSelector =
buildStrictReplicaGroupArSelector(hybridSelector);
+
when(hybridSelector.fetchServerRankingsWithScores(any())).thenReturn(List.of());
+
+ Map<String, List<SegmentInstanceCandidate>> candidatesMap = new
HashMap<>();
+ List<SegmentInstanceCandidate> partition0Candidates = Arrays.asList(
+ new SegmentInstanceCandidate(STRICT_RG0_SERVER_A, true,
FALLBACK_POOL_ID, 0),
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_C, true,
FALLBACK_POOL_ID, 1),
+ new SegmentInstanceCandidate(replicaGroup2ServerE, true,
FALLBACK_POOL_ID, 2));
+ candidatesMap.put(STRICT_SEGMENT0, partition0Candidates);
+ candidatesMap.put(STRICT_SEGMENT1, partition0Candidates);
+ candidatesMap.put(STRICT_SEGMENT2, Arrays.asList(
+ new SegmentInstanceCandidate(STRICT_RG0_SERVER_B, true,
FALLBACK_POOL_ID, 0),
+ new SegmentInstanceCandidate(STRICT_RG1_SERVER_D, true,
FALLBACK_POOL_ID, 1),
+ new SegmentInstanceCandidate(replicaGroup2ServerF, true,
FALLBACK_POOL_ID, 2)));
+ SegmentStates segmentStates = new SegmentStates(candidatesMap, new
HashSet<>(STRICT_SEGMENTS), null);
+
+ InstanceSelector.InstanceMapping result =
+ instanceSelector.select(STRICT_SEGMENTS, Integer.MIN_VALUE,
segmentStates, null);
+
+ // floorMod(Integer.MIN_VALUE, 3) is 1; the previous '%' indexing returned
-2 and threw.
+ assertEquals(result.segmentToInstanceMap(), Map.of(
+ STRICT_SEGMENT0, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT1, STRICT_RG1_SERVER_C,
+ STRICT_SEGMENT2, STRICT_RG1_SERVER_D));
+ }
+}
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
index 42f66b844c0..71298ee7cb3 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java
@@ -1254,6 +1254,13 @@ public class CommonConstants {
public static final String CONFIG_OF_STATS_METRIC_EXPORT_INTERVAL_MS =
CONFIG_PREFIX + ".stats.metric.export.interval.ms";
public static final long DEFAULT_STATS_METRIC_EXPORT_INTERVAL_MS = 10 *
1000;
+
+ // Controls whether replica-group-level adaptive routing is enabled for
StrictReplicaGroupInstanceSelector.
+ // When false, StrictReplicaGroupInstanceSelector falls back to
round-robin even if adaptive server
+ // selection is configured.
+ public static final String CONFIG_OF_STRICT_REPLICA_GROUP_ENABLED =
+ CONFIG_PREFIX + ".strict.replica.group.enabled";
+ public static final boolean DEFAULT_STRICT_REPLICA_GROUP_ENABLED = true;
}
public static class Grpc {
diff --git a/pom.xml b/pom.xml
index c2cf6b1acfc..682494ff0d9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2103,7 +2103,7 @@
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
- <version>2.46.1</version>
+ <version>3.0.0</version>
<executions>
<execution>
<goals>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]