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 ba49142265f Share instance selector candidate lists between segments
with equal assignments (#19785)
ba49142265f is described below
commit ba49142265f93e4eef17c42c6dc262ab9c9e9f5b
Author: Gonzalo Ortiz Jaureguizar <[email protected]>
AuthorDate: Thu Oct 15 10:20:09 2026 +0200
Share instance selector candidate lists between segments with equal
assignments (#19785)
Part of #19782.
---
.../instanceselector/BaseInstanceSelector.java | 248 ++++++++++++++++-----
.../ReplicaGroupInstanceSelector.java | 65 +++---
.../instanceselector/InstanceSelectorTest.java | 189 ++++++++++++++++
3 files changed, 420 insertions(+), 82 deletions(-)
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 6c8d96cbf6d..263dc258457 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
@@ -25,10 +25,10 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
-import java.util.TreeSet;
import javax.annotation.Nullable;
import org.apache.helix.AccessOption;
import org.apache.helix.model.ExternalView;
@@ -50,6 +50,8 @@ import
org.apache.pinot.spi.utils.CommonConstants.Helix.StateModel.SegmentStateM
import org.apache.pinot.spi.utils.builder.TableNameBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.slf4j.Marker;
+import org.slf4j.MarkerFactory;
import static
org.apache.pinot.spi.utils.CommonConstants.Broker.FALLBACK_POOL_ID;
@@ -90,8 +92,18 @@ import static
org.apache.pinot.spi.utils.CommonConstants.Broker.FALLBACK_POOL_ID
/// which one owns the table's gauges.
public abstract class BaseInstanceSelector implements InstanceSelector {
private static final Logger LOGGER =
LoggerFactory.getLogger(BaseInstanceSelector.class);
+ /// Tags the INFO logs that print the whole new-segment map on every rebuild
("Got N new segments: ..."). To drop
+ /// them without code changes, add to the log4j2 config:
+ /// `<MarkerFilter marker="ROUTING_NEW_SEGMENTS" onMatch="DENY"
onMismatch="NEUTRAL"/>`
+ private static final Marker NEW_SEGMENTS_MARKER =
MarkerFactory.getMarker("ROUTING_NEW_SEGMENTS");
// To prevent int overflow, reset the request id once it reaches this value
protected static final long MAX_REQUEST_ID = 1_000_000_000;
+ /// Maximum number of distinct (ideal state, external view) instance state
map pairs whose candidate lists one
+ /// rebuild shares between segments. Past this bound, the rebuild stops
caching new pairs: segments with an already
+ /// cached pair keep sharing its lists, and segments with any other pair get
their own lists, as if there was no
+ /// sharing. In a realtime table, all the segments of a stream partition
usually have the same pair, so the number of
+ /// distinct pairs is close to the number of partitions, far below this
bound.
+ static final int MAX_SHARED_CANDIDATE_LISTS = 4096;
protected TableConfig _tableConfig;
protected String _tableNameWithType;
@@ -126,6 +138,9 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
// computed alongside.
protected volatile TableReplicaHealth _replicaHealth;
protected Map<String, ServerInstance> _enabledServerStore;
+ // Only read by the rebuild thread. Not final so that tests can disable or
bound the sharing.
+ @VisibleForTesting
+ int _maxSharedCandidateLists = MAX_SHARED_CANDIDATE_LISTS;
@Override
public void init(TableConfig tableConfig, ZkHelixPropertyStore<ZNRecord>
propertyStore,
@@ -235,7 +250,8 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
newSegmentCreationTimeMap.put(segmentZKMetadata.getSegmentName(),
creationTimeMs);
}
}
- LOGGER.info("Got {} new segments: {} for table: {} by reading ZK metadata,
current time: {}",
+ LOGGER.info(NEW_SEGMENTS_MARKER,
+ "Got {} new segments: {} for table: {} by reading ZK metadata, current
time: {}",
newSegmentCreationTimeMap.size(), newSegmentCreationTimeMap,
_tableNameWithType, currentTimeMs);
return newSegmentCreationTimeMap;
}
@@ -265,9 +281,9 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
}
/// Returns the online instances for routing purpose.
- static TreeSet<String> getOnlineInstances(Map<String, String>
idealStateInstanceStateMap,
+ static Set<String> getOnlineInstances(Map<String, String>
idealStateInstanceStateMap,
Map<String, String> externalViewInstanceStateMap) {
- TreeSet<String> onlineInstances = new TreeSet<>();
+ Set<String> onlineInstances = new HashSet<>();
// Only track ONLINE/CONSUMING instances within the ideal state
for (Map.Entry<String, String> entry :
idealStateInstanceStateMap.entrySet()) {
String instance = entry.getKey();
@@ -279,13 +295,58 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
return onlineInstances;
}
- /// Converts the given map into a sorted map if needed.
- static SortedMap<String, String> convertToSortedMap(Map<String, String> map)
{
+ /// Returns a map that iterates the entries of the given map in key order:
the map itself when it is a
+ /// [SortedMap] or when its iteration order is already sorted, otherwise a
sorted copy.
+ ///
+ /// The instance state maps read from ZooKeeper are insertion-ordered, and
are usually written in key order, so
+ /// the linear check avoids the copy in the common case.
+ static Map<String, String> sortByKeyIfNeeded(Map<String, String> map) {
if (map instanceof SortedMap) {
- return (SortedMap<String, String>) map;
- } else {
- return new TreeMap<>(map);
+ return map;
}
+ String previousKey = null;
+ for (String key : map.keySet()) {
+ if (previousKey != null && previousKey.compareTo(key) > 0) {
+ return new TreeMap<>(map);
+ }
+ previousKey = key;
+ }
+ return map;
+ }
+
+ /// Returns the candidates of an old segment: the instances that are
ONLINE/CONSUMING in both the ideal state and
+ /// the external view, in instance order.
+ List<SegmentInstanceCandidate> computeOldSegmentCandidates(Map<String,
String> idealStateInstanceStateMap,
+ Map<String, String> externalViewInstanceStateMap) {
+ List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
+ int idealStateReplicaId = 0;
+ for (Map.Entry<String, String> entry :
sortByKeyIfNeeded(idealStateInstanceStateMap).entrySet()) {
+ String instance = entry.getKey();
+ // NOTE: DO NOT check if EV matches IS because it is a valid state when
EV is CONSUMING while IS is ONLINE
+ if (isOnlineForRouting(entry.getValue()) &&
isOnlineForRouting(externalViewInstanceStateMap.get(instance))) {
+ candidates.add(new SegmentInstanceCandidate(instance, true,
getPool(instance), idealStateReplicaId));
+ }
+ idealStateReplicaId++;
+ }
+ return candidates;
+ }
+
+ /// Returns the candidates of a new segment: the instances that are
ONLINE/CONSUMING in the ideal state, in instance
+ /// order. A candidate is online when the instance is ONLINE/CONSUMING in
the external view.
+ List<SegmentInstanceCandidate> computeNewSegmentCandidates(Map<String,
String> idealStateInstanceStateMap,
+ @Nullable Map<String, String> externalViewInstanceStateMap) {
+ List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
+ int idealStateReplicaId = 0;
+ for (Map.Entry<String, String> entry :
sortByKeyIfNeeded(idealStateInstanceStateMap).entrySet()) {
+ if (isOnlineForRouting(entry.getValue())) {
+ String instance = entry.getKey();
+ boolean online =
+ externalViewInstanceStateMap != null &&
isOnlineForRouting(externalViewInstanceStateMap.get(instance));
+ candidates.add(new SegmentInstanceCandidate(instance, online,
getPool(instance), idealStateReplicaId));
+ }
+ idealStateReplicaId++;
+ }
+ return candidates;
}
/// Updates the segment maps based on the given ideal state, external view,
online segments (segments with
@@ -301,60 +362,31 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
Map<String, Map<String, String>> idealStateAssignment =
idealState.getRecord().getMapFields();
Map<String, Map<String, String>> externalViewAssignment =
externalView.getRecord().getMapFields();
+ InstanceStateMapsCache instanceStateMapsCache = new
InstanceStateMapsCache(_maxSharedCandidateLists);
int numSinglePoolSegments = 0;
Set<Integer> pools = new HashSet<>();
for (String segment : onlineSegments) {
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
- // TODO: Verify whether sorting is actually needed
- Map<String, String> sortedIdealStateMap =
convertToSortedMap(idealStateInstanceStateMap);
Long newSegmentCreationTimeMs = newSegmentCreationTimeMap.get(segment);
Map<String, String> externalViewInstanceStateMap =
externalViewAssignment.get(segment);
- if (externalViewInstanceStateMap == null) {
- if (newSegmentCreationTimeMs != null) {
- // New segment
- List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
- int idealStateReplicaId = 0;
- for (Map.Entry<String, String> entry :
sortedIdealStateMap.entrySet()) {
- if (isOnlineForRouting(entry.getValue())) {
- String instance = entry.getKey();
- candidates.add(new SegmentInstanceCandidate(instance, false,
getPool(instance), idealStateReplicaId));
- }
- idealStateReplicaId++;
- }
- _newSegmentStateMap.put(segment, new
NewSegmentState(newSegmentCreationTimeMs, candidates));
- } else {
- // Old segment
- putOldSegment(segment, List.of(), idealStateInstanceStateMap);
- }
+ if (newSegmentCreationTimeMs != null) {
+ // New segment
+ _newSegmentStateMap.put(segment, new
NewSegmentState(newSegmentCreationTimeMs,
+ computeNewSegmentCandidates(idealStateInstanceStateMap,
externalViewInstanceStateMap)));
+ } else if (externalViewInstanceStateMap == null) {
+ // Old segment
+ putOldSegment(segment, List.of(), idealStateInstanceStateMap);
} else {
- TreeSet<String> onlineInstances =
getOnlineInstances(idealStateInstanceStateMap, externalViewInstanceStateMap);
- if (newSegmentCreationTimeMs != null) {
- // New segment
- List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
- int idealStateReplicaId = 0;
- for (Map.Entry<String, String> entry :
sortedIdealStateMap.entrySet()) {
- if (isOnlineForRouting(entry.getValue())) {
- String instance = entry.getKey();
- candidates.add(
- new SegmentInstanceCandidate(instance,
onlineInstances.contains(instance), getPool(instance),
- idealStateReplicaId));
- }
- idealStateReplicaId++;
- }
- _newSegmentStateMap.put(segment, new
NewSegmentState(newSegmentCreationTimeMs, candidates));
- } else {
- // Old segment
- List<SegmentInstanceCandidate> candidates = new
ArrayList<>(onlineInstances.size());
- int idealStateReplicaId = 0;
- for (String instance : sortedIdealStateMap.keySet()) {
- if (onlineInstances.contains(instance)) {
- candidates.add(new SegmentInstanceCandidate(instance, true,
getPool(instance), idealStateReplicaId));
- }
- idealStateReplicaId++;
- }
- putOldSegment(segment, candidates, idealStateInstanceStateMap);
+ // Old segment. Segments with the same instance state maps share the
candidate list.
+ InstanceStateMaps instanceStateMaps =
+ instanceStateMapsCache.canonicalize(idealStateInstanceStateMap,
externalViewInstanceStateMap);
+ List<SegmentInstanceCandidate> candidates =
instanceStateMaps._oldSegmentCandidates;
+ if (candidates == null) {
+ candidates = computeOldSegmentCandidates(idealStateInstanceStateMap,
externalViewInstanceStateMap);
+ instanceStateMaps._oldSegmentCandidates = candidates;
}
+ putOldSegment(segment, candidates, idealStateInstanceStateMap);
}
if (_emitSinglePoolSegmentsMetric) {
pools.clear();
@@ -443,10 +475,33 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
unavailableSegments.size());
}
+ /// Returns the enabled candidates, and adds their instances to the serving
instances.
+ ///
+ /// When every candidate is enabled, returns the given list itself rather
than a copy. That publishes the list to
+ /// the query path, which is safe because candidate lists are never modified
once built. It also keeps the lists
+ /// that [#updateSegmentMaps] shares between segments shared in the
published [SegmentStates].
private List<SegmentInstanceCandidate>
getEnabledCandidatesAndAddToServingInstances(
List<SegmentInstanceCandidate> candidates, Set<String> servingInstances)
{
- List<SegmentInstanceCandidate> enabledCandidates = new
ArrayList<>(candidates.size());
- for (SegmentInstanceCandidate candidate : candidates) {
+ int numCandidates = candidates.size();
+ int numLeadingEnabledCandidates = 0;
+ while (numLeadingEnabledCandidates < numCandidates) {
+ String instance =
candidates.get(numLeadingEnabledCandidates).getInstance();
+ if (!_enabledInstances.contains(instance)) {
+ break;
+ }
+ servingInstances.add(instance);
+ numLeadingEnabledCandidates++;
+ }
+ if (numLeadingEnabledCandidates == numCandidates) {
+ return candidates;
+ }
+ List<SegmentInstanceCandidate> enabledCandidates = new
ArrayList<>(numCandidates - 1);
+ for (int i = 0; i < numLeadingEnabledCandidates; i++) {
+ enabledCandidates.add(candidates.get(i));
+ }
+ // The candidate at numLeadingEnabledCandidates is disabled
+ for (int i = numLeadingEnabledCandidates + 1; i < numCandidates; i++) {
+ SegmentInstanceCandidate candidate = candidates.get(i);
String instance = candidate.getInstance();
if (_enabledInstances.contains(instance)) {
enabledCandidates.add(candidate);
@@ -505,7 +560,8 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
}
}
}
- LOGGER.info("Got {} new segments: {} for table: {} by processing existing
states, current time: {}",
+ LOGGER.info(NEW_SEGMENTS_MARKER,
+ "Got {} new segments: {} for table: {} by processing existing states,
current time: {}",
newSegmentCreationTimeMap.size(), newSegmentCreationTimeMap,
_tableNameWithType, currentTimeMs);
return newSegmentCreationTimeMap;
}
@@ -559,4 +615,82 @@ public abstract class BaseInstanceSelector implements
InstanceSelector {
/// servers and let servers decide how to handle them.
protected abstract InstanceMapping select(List<String> segments, int
requestId, SegmentStates segmentStates,
Map<String, String> queryOptions);
+
+ /// The ideal state and external view instance state maps of a segment,
compared by content. Within one rebuild,
+ /// segments with equal maps get the same routing candidates, so the
canonical instance (see
+ /// [InstanceStateMapsCache]) caches what is derived from the maps, and the
segments share it.
+ ///
+ /// Not thread safe. Only used by the rebuild thread, for the duration of
one rebuild.
+ static final class InstanceStateMaps {
+ final Map<String, String> _idealStateInstanceStateMap;
+ @Nullable
+ final Map<String, String> _externalViewInstanceStateMap;
+ private final int _hashCode;
+ // Derived from the maps, computed on first use
+ @Nullable
+ Set<String> _onlineInstances;
+ @Nullable
+ List<SegmentInstanceCandidate> _oldSegmentCandidates;
+ // Used by the strict replica-group selector to collect the unavailable
instances once per instance state maps
+ boolean _unavailableInstancesCollected;
+
+ InstanceStateMaps(Map<String, String> idealStateInstanceStateMap,
+ @Nullable Map<String, String> externalViewInstanceStateMap) {
+ _idealStateInstanceStateMap = idealStateInstanceStateMap;
+ _externalViewInstanceStateMap = externalViewInstanceStateMap;
+ _hashCode = 31 * idealStateInstanceStateMap.hashCode() +
Objects.hashCode(externalViewInstanceStateMap);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof InstanceStateMaps)) {
+ return false;
+ }
+ InstanceStateMaps that = (InstanceStateMaps) o;
+ return _hashCode == that._hashCode &&
_idealStateInstanceStateMap.equals(that._idealStateInstanceStateMap)
+ && Objects.equals(_externalViewInstanceStateMap,
that._externalViewInstanceStateMap);
+ }
+
+ @Override
+ public int hashCode() {
+ return _hashCode;
+ }
+ }
+
+ /// Returns one canonical [InstanceStateMaps] per distinct pair of instance
state maps, so that the segments of one
+ /// rebuild with equal maps share what is derived from them.
+ ///
+ /// The cache keeps at most `maxSize` pairs. Past that bound, it stops
caching new pairs: a pair already in the cache
+ /// still returns its canonical [InstanceStateMaps], while any other pair
returns a new [InstanceStateMaps] for each
+ /// call, which makes each segment compute its own state, as if there was no
sharing.
+ ///
+ /// Not thread safe. Only used by the rebuild thread, for the duration of
one rebuild.
+ static final class InstanceStateMapsCache {
+ private final int _maxSize;
+ private final Map<InstanceStateMaps, InstanceStateMaps> _cache = new
HashMap<>();
+
+ InstanceStateMapsCache(int maxSize) {
+ _maxSize = maxSize;
+ }
+
+ InstanceStateMaps canonicalize(Map<String, String>
idealStateInstanceStateMap,
+ @Nullable Map<String, String> externalViewInstanceStateMap) {
+ InstanceStateMaps instanceStateMaps =
+ new InstanceStateMaps(idealStateInstanceStateMap,
externalViewInstanceStateMap);
+ if (_maxSize <= 0) {
+ return instanceStateMaps;
+ }
+ InstanceStateMaps canonical = _cache.get(instanceStateMaps);
+ if (canonical != null) {
+ return canonical;
+ }
+ if (_cache.size() < _maxSize) {
+ _cache.put(instanceStateMaps, instanceStateMaps);
+ }
+ return instanceStateMaps;
+ }
+ }
}
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 038994d40cd..9f1be381206 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
@@ -208,33 +208,42 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
Map<String, Map<String, String>> idealStateAssignment =
idealState.getRecord().getMapFields();
Map<String, Map<String, String>> externalViewAssignment =
externalView.getRecord().getMapFields();
- // Get the online instances for the segments
- Map<String, Set<String>> oldSegmentToOnlineInstancesMap =
+ // Get the online instances for the segments. Segments with the same
instance state maps share one
+ // InstanceStateMaps, which caches the online instances and the old
segment candidates derived from the maps.
+ InstanceStateMapsCache instanceStateMapsCache = new
InstanceStateMapsCache(_maxSharedCandidateLists);
+ Map<String, InstanceStateMaps> oldSegmentToInstanceStateMaps =
new HashMap<>(HashUtil.getHashMapCapacity(onlineSegments.size()));
- Map<String, Set<String>> newSegmentToOnlineInstancesMap = new
HashMap<>(newSegmentMapCapacity);
+ Map<String, InstanceStateMaps> newSegmentToInstanceStateMaps = new
HashMap<>(newSegmentMapCapacity);
for (String segment : onlineSegments) {
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
assert idealStateInstanceStateMap != null;
Map<String, String> externalViewInstanceStateMap =
externalViewAssignment.get(segment);
- Set<String> onlineInstances;
- if (externalViewInstanceStateMap == null) {
- onlineInstances = Set.of();
- } else {
- onlineInstances = getOnlineInstances(idealStateInstanceStateMap,
externalViewInstanceStateMap);
+ InstanceStateMaps instanceStateMaps =
+ instanceStateMapsCache.canonicalize(idealStateInstanceStateMap,
externalViewInstanceStateMap);
+ if (instanceStateMaps._onlineInstances == null) {
+ instanceStateMaps._onlineInstances = externalViewInstanceStateMap ==
null
+ ? Set.of()
+ : getOnlineInstances(idealStateInstanceStateMap,
externalViewInstanceStateMap);
}
if (newSegmentCreationTimeMap.containsKey(segment)) {
- newSegmentToOnlineInstancesMap.put(segment, onlineInstances);
+ newSegmentToInstanceStateMaps.put(segment, instanceStateMaps);
} else {
- oldSegmentToOnlineInstancesMap.put(segment, onlineInstances);
+ oldSegmentToInstanceStateMaps.put(segment, instanceStateMaps);
}
}
// Calculate the unavailable instances based on the old segments' online
instances for each combination of instances
// in the ideal state
Map<Set<String>, Set<String>> unavailableInstancesMap = new HashMap<>();
- for (Map.Entry<String, Set<String>> entry :
oldSegmentToOnlineInstancesMap.entrySet()) {
+ for (Map.Entry<String, InstanceStateMaps> entry :
oldSegmentToInstanceStateMaps.entrySet()) {
String segment = entry.getKey();
- Set<String> onlineInstances = entry.getValue();
+ InstanceStateMaps instanceStateMaps = entry.getValue();
+ if (instanceStateMaps._unavailableInstancesCollected) {
+ // Another segment with the same instance state maps already collected
the same unavailable instances
+ continue;
+ }
+ instanceStateMaps._unavailableInstancesCollected = true;
+ Set<String> onlineInstances = instanceStateMaps._onlineInstances;
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
Set<String> instancesInIdealState = idealStateInstanceStateMap.keySet();
Set<String> unavailableInstances =
@@ -252,30 +261,36 @@ public class ReplicaGroupInstanceSelector extends
BaseInstanceSelector {
}
// Iterate over the maps and exclude the unavailable instances
- for (Map.Entry<String, Set<String>> entry :
oldSegmentToOnlineInstancesMap.entrySet()) {
+ for (Map.Entry<String, InstanceStateMaps> entry :
oldSegmentToInstanceStateMaps.entrySet()) {
String segment = entry.getKey();
- // NOTE: onlineInstances is either a TreeSet or an EmptySet (sorted)
- Set<String> onlineInstances = entry.getValue();
+ InstanceStateMaps instanceStateMaps = 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;
- for (String instance :
convertToSortedMap(idealStateInstanceStateMap).keySet()) {
- if (onlineInstances.contains(instance) &&
!unavailableInstances.contains(instance)) {
- candidates.add(new SegmentInstanceCandidate(instance, true,
getPool(instance), idealStateReplicaId));
+ // The candidates only depend on the instance state maps: the
unavailable instances are a function of the
+ // instances in the ideal state, and they are final once computed above
+ List<SegmentInstanceCandidate> candidates =
instanceStateMaps._oldSegmentCandidates;
+ if (candidates == null) {
+ Set<String> onlineInstances = instanceStateMaps._onlineInstances;
+ Set<String> unavailableInstances =
unavailableInstancesMap.get(idealStateInstanceStateMap.keySet());
+ candidates = new ArrayList<>(onlineInstances.size());
+ int idealStateReplicaId = 0;
+ for (String instance :
sortByKeyIfNeeded(idealStateInstanceStateMap).keySet()) {
+ if (onlineInstances.contains(instance) &&
!unavailableInstances.contains(instance)) {
+ candidates.add(new SegmentInstanceCandidate(instance, true,
getPool(instance), idealStateReplicaId));
+ }
+ idealStateReplicaId++;
}
- idealStateReplicaId++;
+ instanceStateMaps._oldSegmentCandidates = candidates;
}
// Instances taken out of service for the whole replica group are
excluded above, so measuring against
// the ideal state count is what makes the replica health metrics
reflect a group-wide knockout.
putOldSegment(segment, candidates, idealStateInstanceStateMap);
}
- for (Map.Entry<String, Set<String>> entry :
newSegmentToOnlineInstancesMap.entrySet()) {
+ for (Map.Entry<String, InstanceStateMaps> entry :
newSegmentToInstanceStateMaps.entrySet()) {
String segment = entry.getKey();
- Set<String> onlineInstances = entry.getValue();
+ Set<String> onlineInstances = entry.getValue()._onlineInstances;
Map<String, String> idealStateInstanceStateMap =
idealStateAssignment.get(segment);
- Map<String, String> sortedIdealStateInstanceStateMap =
convertToSortedMap(idealStateInstanceStateMap);
+ Map<String, String> sortedIdealStateInstanceStateMap =
sortByKeyIfNeeded(idealStateInstanceStateMap);
Set<String> unavailableInstances =
unavailableInstancesMap.getOrDefault(idealStateInstanceStateMap.keySet(),
Set.of());
List<SegmentInstanceCandidate> candidates = new
ArrayList<>(idealStateInstanceStateMap.size());
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 64bed8992ea..421f3fa9288 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
@@ -29,12 +29,14 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.model.ExternalView;
@@ -81,7 +83,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
@@ -2406,4 +2410,189 @@ public class InstanceSelectorTest {
assertEquals(first.getSegmentToInstanceMap(), expectedInstances);
assertEquals(first.getOptionalSegmentToInstanceMap(), Map.of("optional",
"instance128"));
}
+
+ @Test
+ public void testSortByKeyIfNeeded() {
+ Map<String, String> sorted = new LinkedHashMap<>();
+ sorted.put("a", ONLINE);
+ sorted.put("b", ONLINE);
+ sorted.put("c", ONLINE);
+ assertSame(BaseInstanceSelector.sortByKeyIfNeeded(sorted), sorted);
+
+ Map<String, String> treeMap = new TreeMap<>(sorted);
+ assertSame(BaseInstanceSelector.sortByKeyIfNeeded(treeMap), treeMap);
+
+ Map<String, String> unsorted = new LinkedHashMap<>();
+ unsorted.put("c", ONLINE);
+ unsorted.put("a", ONLINE);
+ unsorted.put("b", ONLINE);
+ Map<String, String> sortedCopy =
BaseInstanceSelector.sortByKeyIfNeeded(unsorted);
+ assertNotSame(sortedCopy, unsorted);
+ assertEquals(new ArrayList<>(sortedCopy.keySet()), List.of("a", "b", "c"));
+ assertEquals(sortedCopy, unsorted);
+ }
+
+ /// Segments with equal ideal state and external view instance state maps
share one candidate list within a
+ /// rebuild. Checks that the sharing does not change any routing outcome, by
comparing a selector that shares with
+ /// one that builds a list per segment, and that the candidates keep the
sorted instance order when the instance
+ /// state maps are not in sorted order.
+ @Test(dataProvider = "selectorType")
+ public void testSharedCandidateListsMatchPerSegmentLists(String
selectorType) {
+ // Partition 0: the ideal state lists the instances out of order. One of
its old segments has a replica OFFLINE in
+ // the external view.
+ // Partition 1: one of its instances is disabled.
+ String[] partition0Instances = {"instance_c", "instance_a", "instance_b"};
+ String[] partition1Instances = {"instance_d", "instance_e", "instance_f"};
+ Set<String> enabledInstances =
+ Set.of("instance_a", "instance_b", "instance_c", "instance_d",
"instance_e", "instance_g");
+
+ Map<String, Map<String, String>> idealStateMaps = new LinkedHashMap<>();
+ Map<String, Map<String, String>> externalViewMaps = new LinkedHashMap<>();
+ for (int i = 0; i < 4; i++) {
+ idealStateMaps.put("p0_" + i, instanceStateMap(partition0Instances,
ONLINE, ONLINE, ONLINE));
+ externalViewMaps.put("p0_" + i, i == 3 ?
instanceStateMap(partition0Instances, ONLINE, ONLINE, OFFLINE)
+ : instanceStateMap(partition0Instances, ONLINE, ONLINE, ONLINE));
+ }
+ for (int i = 0; i < 3; i++) {
+ String state = i == 2 ? CONSUMING : ONLINE;
+ idealStateMaps.put("p1_" + i, instanceStateMap(partition1Instances,
state, state, state));
+ externalViewMaps.put("p1_" + i, instanceStateMap(partition1Instances,
state, state, state));
+ }
+ // Old segment missing from the external view: unavailable
+ idealStateMaps.put("p2_0", instanceStateMap(new String[]{"instance_g"},
ONLINE));
+ IdealState initialIdealState =
createInsertionOrderedIdealState(idealStateMaps);
+ ExternalView initialExternalView =
createInsertionOrderedExternalView(externalViewMaps);
+ Set<String> initialOnlineSegments = new HashSet<>(idealStateMaps.keySet());
+
+ // New segments, first seen by the assignment change: one missing from the
external view, one partially online
+ idealStateMaps.put("new_0", instanceStateMap(partition0Instances, ONLINE,
ONLINE, ONLINE));
+ idealStateMaps.put("new_1", instanceStateMap(partition0Instances, ONLINE,
ONLINE, ONLINE));
+ externalViewMaps.put("new_1", Map.of("instance_a", ONLINE));
+ IdealState idealState = createInsertionOrderedIdealState(idealStateMaps);
+ ExternalView externalView =
createInsertionOrderedExternalView(externalViewMaps);
+ Set<String> onlineSegments = new HashSet<>(idealStateMaps.keySet());
+
+ BaseInstanceSelector sharingSelector = (BaseInstanceSelector)
createTestInstanceSelector(selectorType,
+ enabledInstances, initialIdealState, initialExternalView,
initialOnlineSegments);
+ sharingSelector.onAssignmentChange(idealState, externalView,
onlineSegments);
+ BaseInstanceSelector perSegmentSelector = (BaseInstanceSelector)
createTestInstanceSelector(selectorType,
+ enabledInstances, initialIdealState, initialExternalView,
initialOnlineSegments);
+ perSegmentSelector._maxSharedCandidateLists = 0;
+ perSegmentSelector.onAssignmentChange(idealState, externalView,
onlineSegments);
+ // Shares the lists of one pair of instance state maps only, so that one
rebuild mixes shared and own lists
+ BaseInstanceSelector boundedSelector = (BaseInstanceSelector)
createTestInstanceSelector(selectorType,
+ enabledInstances, initialIdealState, initialExternalView,
initialOnlineSegments);
+ boundedSelector._maxSharedCandidateLists = 1;
+ boundedSelector.onAssignmentChange(idealState, externalView,
onlineSegments);
+
+ // Same candidates for every segment
+ SegmentStates sharedStates = sharingSelector._segmentStates;
+ SegmentStates perSegmentStates = perSegmentSelector._segmentStates;
+ SegmentStates boundedStates = boundedSelector._segmentStates;
+ for (String segment : onlineSegments) {
+ assertEquals(describe(sharedStates.getCandidates(segment)),
describe(perSegmentStates.getCandidates(segment)),
+ segment);
+ assertEquals(describe(boundedStates.getCandidates(segment)),
describe(perSegmentStates.getCandidates(segment)),
+ segment);
+ }
+ assertEquals(boundedStates.getUnavailableSegments(),
perSegmentStates.getUnavailableSegments());
+ assertEquals(boundedStates.getServingInstances(),
perSegmentStates.getServingInstances());
+ assertEquals(sharedStates.getUnavailableSegments(), Set.of("p2_0"));
+ assertEquals(sharedStates.getUnavailableSegments(),
perSegmentStates.getUnavailableSegments());
+ assertEquals(sharedStates.getServingInstances(),
perSegmentStates.getServingInstances());
+ TableReplicaHealth sharedHealth = sharingSelector.getReplicaHealth();
+ TableReplicaHealth perSegmentHealth =
perSegmentSelector.getReplicaHealth();
+ assertEquals(sharedHealth.getMinPercentOfReplicas(),
perSegmentHealth.getMinPercentOfReplicas());
+ assertEquals(sharedHealth.getNumSegmentsAtMinPercentOfReplicas(),
+ perSegmentHealth.getNumSegmentsAtMinPercentOfReplicas());
+ assertEquals(sharedHealth.getNumUnavailableSegments(),
perSegmentHealth.getNumUnavailableSegments());
+
+ // Candidates are in sorted instance order, with the replica id of the
sorted ideal state, whatever the order of
+ // the instance state maps
+ boolean strict =
selectorType.equals(STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE);
+ // Strict replica-group takes instance_b out of service for the whole
partition, because it is OFFLINE for p0_3
+ assertEquals(describe(sharedStates.getCandidates("p0_0")),
+ strict ? List.of("instance_a/0/true", "instance_c/2/true")
+ : List.of("instance_a/0/true", "instance_b/1/true",
"instance_c/2/true"));
+ assertEquals(describe(sharedStates.getCandidates("p0_3")),
List.of("instance_a/0/true", "instance_c/2/true"));
+ // instance_f is disabled
+ assertEquals(describe(sharedStates.getCandidates("p1_0")),
List.of("instance_d/0/true", "instance_e/1/true"));
+ assertEquals(describe(sharedStates.getCandidates("new_0")),
+ strict ? List.of("instance_a/0/false", "instance_c/2/false")
+ : List.of("instance_a/0/false", "instance_b/1/false",
"instance_c/2/false"));
+ assertEquals(describe(sharedStates.getCandidates("new_1")),
+ strict ? List.of("instance_a/0/true", "instance_c/2/false")
+ : List.of("instance_a/0/true", "instance_b/1/false",
"instance_c/2/false"));
+
+ // The sharing selector shares the lists of segments with equal instance
state maps, the other one does not
+ assertSame(sharedStates.getCandidates("p0_0"),
sharedStates.getCandidates("p0_1"));
+ assertSame(sharedStates.getCandidates("p0_0"),
sharedStates.getCandidates("p0_2"));
+ assertNotSame(perSegmentStates.getCandidates("p0_0"),
perSegmentStates.getCandidates("p0_1"));
+
+ // Same selection for every request
+ List<String> segments = new ArrayList<>(idealStateMaps.keySet());
+ for (long requestId = 0; requestId < 6; requestId++) {
+ InstanceSelector.SelectionResult shared =
sharingSelector.select(_brokerRequest, segments, requestId);
+ InstanceSelector.SelectionResult perSegment =
perSegmentSelector.select(_brokerRequest, segments, requestId);
+ InstanceSelector.SelectionResult bounded =
boundedSelector.select(_brokerRequest, segments, requestId);
+ assertEquals(shared.getSegmentToInstanceMap(),
perSegment.getSegmentToInstanceMap());
+ assertEquals(shared.getOptionalSegmentToInstanceMap(),
perSegment.getOptionalSegmentToInstanceMap());
+ assertEquals(shared.getUnavailableSegments(),
perSegment.getUnavailableSegments());
+ assertEquals(shared.getUnavailableSegments(), List.of("p2_0"));
+ assertEquals(bounded.getSegmentToInstanceMap(),
perSegment.getSegmentToInstanceMap());
+ assertEquals(bounded.getOptionalSegmentToInstanceMap(),
perSegment.getOptionalSegmentToInstanceMap());
+ }
+
+ // Enabling the disabled instance keeps both selectors in step
+ Set<String> allInstances = new HashSet<>(enabledInstances);
+ allInstances.add("instance_f");
+ sharingSelector.onInstancesChange(allInstances, List.of("instance_f"));
+ perSegmentSelector.onInstancesChange(allInstances, List.of("instance_f"));
+
assertEquals(describe(sharingSelector._segmentStates.getCandidates("p1_0")),
+ List.of("instance_d/0/true", "instance_e/1/true",
"instance_f/2/true"));
+ assertSame(sharingSelector._segmentStates.getCandidates("p1_0"),
+ sharingSelector._segmentStates.getCandidates("p1_1"));
+ for (long requestId = 0; requestId < 6; requestId++) {
+ InstanceSelector.SelectionResult shared =
sharingSelector.select(_brokerRequest, segments, requestId);
+ InstanceSelector.SelectionResult perSegment =
perSegmentSelector.select(_brokerRequest, segments, requestId);
+ assertEquals(shared.getSegmentToInstanceMap(),
perSegment.getSegmentToInstanceMap());
+ assertEquals(shared.getOptionalSegmentToInstanceMap(),
perSegment.getOptionalSegmentToInstanceMap());
+ }
+ }
+
+ /// Returns an insertion-ordered instance state map, keeping the order of
the given instances.
+ private static Map<String, String> instanceStateMap(String[] instances,
String... states) {
+ Map<String, String> instanceStateMap = new LinkedHashMap<>();
+ for (int i = 0; i < instances.length; i++) {
+ instanceStateMap.put(instances[i], states[i]);
+ }
+ return instanceStateMap;
+ }
+
+ private static IdealState createInsertionOrderedIdealState(Map<String,
Map<String, String>> instanceStateMaps) {
+ IdealState idealState = new IdealState(TABLE_NAME);
+ for (Map.Entry<String, Map<String, String>> entry :
instanceStateMaps.entrySet()) {
+ idealState.getRecord().setMapField(entry.getKey(), new
LinkedHashMap<>(entry.getValue()));
+ }
+ return idealState;
+ }
+
+ private static ExternalView createInsertionOrderedExternalView(Map<String,
Map<String, String>> instanceStateMaps) {
+ ExternalView externalView = new ExternalView(TABLE_NAME);
+ for (Map.Entry<String, Map<String, String>> entry :
instanceStateMaps.entrySet()) {
+ externalView.getRecord().setMapField(entry.getKey(), new
LinkedHashMap<>(entry.getValue()));
+ }
+ return externalView;
+ }
+
+ private static List<String> describe(@Nullable
List<SegmentInstanceCandidate> candidates) {
+ if (candidates == null) {
+ return null;
+ }
+ List<String> descriptions = new ArrayList<>(candidates.size());
+ for (SegmentInstanceCandidate candidate : candidates) {
+ descriptions.add(candidate.getInstance() + "/" +
candidate.getIdealStateReplicaId() + "/" + candidate.isOnline());
+ }
+ return descriptions;
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]