yashmayya commented on code in PR #19221:
URL: https://github.com/apache/pinot/pull/19221#discussion_r3770433904
##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java:
##########
@@ -326,12 +409,25 @@ void refreshSegmentStates() {
new
HashMap<>(HashUtil.getHashMapCapacity(_oldSegmentCandidatesMap.size() +
_newSegmentStateMap.size()));
Set<String> servingInstances = new HashSet<>();
Set<String> unavailableSegments = new HashSet<>();
+ int minPercentOfReplicas = SegmentReplicaHealth.FULLY_REPLICATED_PERCENT;
+ int numSegmentsWithoutRedundancy = 0;
for (Map.Entry<String, List<SegmentInstanceCandidate>> entry :
_oldSegmentCandidatesMap.entrySet()) {
String segment = entry.getKey();
List<SegmentInstanceCandidate> candidates = entry.getValue();
List<SegmentInstanceCandidate> enabledCandidates =
getEnabledCandidatesAndAddToServingInstances(candidates,
servingInstances);
+ if (_emitReplicaHealthMetrics) {
+ int expectedReplicas = getExpectedReplicas(segment, candidates.size());
+ int servingReplicas = enabledCandidates.size();
+ if (SegmentReplicaHealth.shouldMeasure(expectedReplicas)) {
+ minPercentOfReplicas = Math.min(minPercentOfReplicas,
+ SegmentReplicaHealth.toPercent(servingReplicas,
expectedReplicas));
Review Comment:
Why is the percentage metric gated on whether the segment is expected to
have more than 1 replica? Even for single replica segments, it would be
valuable to know whether it's 0% or 100%, no?
##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentReplicaHealth.java:
##########
@@ -0,0 +1,92 @@
+/**
+ * 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 javax.annotation.concurrent.Immutable;
+
+
+/// Per-segment view of how well a table's segments are replicated across the
servers this broker can
Review Comment:
> Per-segment view
It's actually a per table view right? The class name also should reflect
that.
##########
pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java:
##########
@@ -1927,4 +1934,511 @@ public void testReplicaGroupAdaptiveServerSelector() {
assertEquals(selectedResult.getLeft(), expectedSelection);
}
+
+ // Replica health metrics
+ //
+ // The scenarios below all use the same three instances and assert on the
SegmentReplicaHealth the
+ // selector derives, since that is what the gauges are emitted from. Each
segment's percentage is
+ // measured against the replicas its own ideal state assigns, so segments do
not have to be uniformly
+ // replicated for the numbers to make sense.
+
+ private static final String REPLICA_INSTANCE_0 = "instance0";
+ private static final String REPLICA_INSTANCE_1 = "instance1";
+ private static final String REPLICA_INSTANCE_2 = "instance2";
+ private static final Set<String> REPLICA_INSTANCES =
+ ImmutableSet.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+
+ /// Returns the ideal state assignment placing the segment on all three
instances as ONLINE.
+ private static List<Pair<String, String>> allOnline() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_2, ONLINE));
+ }
+
+ /// Returns an assignment placing the segment on two of the three instances
as ONLINE.
+ private static List<Pair<String, String>> twoReplicas() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE));
+ }
+
+ /// Returns an external view assignment where the first `numOnline` of the
three instances are ONLINE
+ /// and the rest are OFFLINE, so the segment looks partially loaded.
+ private static List<Pair<String, String>> partiallyOnline(int numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : OFFLINE));
+ }
+ return assignment;
+ }
+
+ private BaseInstanceSelector createReplicaHealthSelector(String
selectorType, Set<String> enabledInstances,
+ Map<String, List<Pair<String, String>>> idealStateAssignment,
+ Map<String, List<Pair<String, String>>> externalViewAssignment) {
+ // Sorted so that the order the selector looks up segment metadata in is
deterministic, which is what
+ // the stub set up by createSegmentCreationTimes matches on
+ return (BaseInstanceSelector) createTestInstanceSelector(selectorType,
enabledInstances,
+ createIdealState(idealStateAssignment),
createExternalView(externalViewAssignment),
+ new TreeSet<>(externalViewAssignment.keySet()));
+ }
+
+ /// Stubs the segment metadata lookup with the given creation times, in the
order the selector reads them.
+ private void createSegmentCreationTimes(Map<String, Long>
creationTimeMsBySegment) {
+ List<Pair<String, Long>> creationTimes = new
ArrayList<>(creationTimeMsBySegment.size());
+ for (String segment : new TreeSet<>(creationTimeMsBySegment.keySet())) {
+ creationTimes.add(new ImmutablePair<>(segment,
creationTimeMsBySegment.get(segment)));
+ }
+ createSegments(creationTimes);
+ }
+
+ /// Marks the given segments as created long enough ago that they are no
longer treated as new, so that
+ /// they count towards the replica health even though their external view
has not converged.
+ private void createOldSegments(List<String> segments) {
+ long creationTimeMs = _mutableClock.millis() -
NEW_SEGMENT_EXPIRATION_MILLIS - 1;
+ Map<String, Long> creationTimes = new HashMap<>();
+ for (String segment : segments) {
+ creationTimes.put(segment, creationTimeMs);
+ }
+ createSegmentCreationTimes(creationTimes);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthFullyReplicated(String selectorType) {
+ // Every segment is ONLINE everywhere the ideal state assigns it
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", allOnline(), "segment1", allOnline()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ // Nothing is degraded, so no expected replica count has to be remembered
+ assertTrue(selector._oldSegmentExpectedReplicasMap.isEmpty());
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ /// Returns an external view assignment with the first `numOnline` of the
three instances ONLINE and the
+ /// rest in ERROR, i.e. replicas that failed their state transition rather
than merely being offline.
+ private static List<Pair<String, String>> partiallyOnlineRestInError(int
numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : ERROR));
+ }
+ return assignment;
+ }
+
+ private static List<Pair<String, String>> singleReplica(String state) {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, state));
+ }
+
+ /// Returns an ideal state assignment placing the segment on all three
instances as CONSUMING, i.e. a
+ /// segment the controller still considers in progress.
+ private static List<Pair<String, String>> allConsuming() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, CONSUMING),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING));
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testConsumingSegmentMeasuredLikeAnyOther(String selectorType) {
+ // Consuming segments are deliberately not special-cased. A partition
whose replicas are all gone will also
+ // raise an ingestion alert, and reconciling that overlap belongs in the
alerting pipeline rather than in a
+ // metric that would otherwise stop meaning what its name says.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
partiallyOnline(0)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testHealthyConsumingSegmentReportsFullyReplicated(String
selectorType) {
+ // The flip side of not excluding them: a partition consuming normally on
every replica must read 100%, or
+ // every real-time table would look permanently degraded. CONSUMING counts
as serving for routing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
allConsuming()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testCommittedSegmentCountedWhilePeersStillDownloading(String
selectorType) {
+ // The exclusion has to end at the commit, not when the last replica
finishes downloading. The ideal state
+ // turns ONLINE at commit while peers still report CONSUMING, and that
segment is an ordinary immutable
+ // one whose replicas are genuinely missing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ // Committed: ideal state ONLINE everywhere
+ Map.of("segment0", allOnline()),
+ // Only the committer has it; the peers have dropped out rather than
reporting CONSUMING
+ Map.of("segment0", partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
+ 1));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testCommittingSegmentStillRoutableFromPeersReportingConsuming(String
selectorType) {
+ // The normal commit window: ideal state ONLINE, peers still CONSUMING in
the external view. They remain
+ // routable, so nothing is short of replicas and no clock starts.
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING))));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+
+
+
+
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testWithoutRedundancyIgnoresSingleReplicaSegments(String
selectorType) {
+ // A segment the ideal state assigns one replica has no redundancy to
lose, so it must never be counted -
+ // otherwise a table that is single-replica by design reads as permanently
at risk.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", singleReplica(ONLINE)), Map.of("segment0",
singleReplica(OFFLINE)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ @Test
+ public void testWithoutRedundancyCountsLastReplicaNotMerelyShort() {
+ // The whole point of a percentage: 2 of 3 is 66% and passes, 1 of 3 is
33% and does not. Pinned because
+ // this is the line the controller-side alert drew, and moving it silently
would change what pages.
+ // Balanced routing only: under strict replica groups segment1's gaps
would exclude those groups for
+ // segment0 as well, taking it to 1 of 3 and hiding the threshold this
asserts on.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", partiallyOnline(2), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+
assertEquals(selector.getReplicaHealth().getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentOnItsLastReplica(String
selectorType) {
+ // This is where the count parts company with the alert's percentage. 1 of
2 is 50%, above the threshold,
+ // so the percentage leaves it alone - but the segment genuinely has no
redundancy, and nothing alerts on
+ // the count, so it is reported. Both segments count: segment0 at 1 of 2
and segment1 at 1 of 3.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas(), "segment1", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE)), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 2);
+ // The percentage still reports the worst of the two, which is the 1-of-3
segment
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 33);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentWithNoReplicaLeft(String
selectorType) {
+ // Leaving 1 of 2 alone must not extend to 0 of 2: the data is gone, and
0% is below the threshold like
+ // any other total loss. A replica-count rule keyed on "three or more
assigned" would miss this.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
OFFLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE))));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ long nowMs = _mutableClock.millis();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+ @Test
+ public void testWithoutRedundancyWatchesReplicatedPartOfMixedTable() {
+ // A real-time table whose consuming segment lives on one replica while
its completed segments live on
+ // three. Losing two replicas of the completed segment has to be reported,
and the thinly replicated
+ // consuming segment must not stop that from happening.
+ createOldSegments(List.of("completed"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("consuming", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
CONSUMING)), "completed", allOnline()),
+ Map.of("consuming", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
CONSUMING)), "completed",
+ partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthPartiallyReplicated(String selectorType) {
+ // segment0 is only loaded on 1 of its 3 replicas, which is the threshold
the low replica alert fires on
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0",
partiallyOnline(1)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 33);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthUnavailableSegment(String selectorType) {
+ // segment0 is loaded nowhere
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0",
partiallyOnline(0)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ // An unavailable segment is not also counted as under-replicated
+ }
+
+ @Test
+ public void testReplicaHealthReportsWorstSegmentNotAverage() {
+ // segment0 is loaded nowhere while the other two are fully loaded.
Reporting the minimum is what keeps
+ // a single unservable segment from being diluted by a large healthy table.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline(), "segment2",
allOnline()),
+ Map.of("segment0", partiallyOnline(0), "segment1", allOnline(),
"segment2", allOnline()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthIgnoresNewSegments(String selectorType) {
+ // segment0 was just created and is only loaded on 1 replica. That is
expected right after a push, so
+ // it must not drag the percentage down.
+ createSegmentCreationTimes(Map.of("segment0", _mutableClock.millis()));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0",
partiallyOnline(1)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthAccountsForDisabledInstance(String
selectorType) {
+ // A disabled instance reduces the replicas the broker can route to just
as much as a missing external
+ // view entry does, and it arrives through onInstancesChange rather than
an assignment change.
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0", allOnline()));
+ assertEquals(selector.getReplicaHealth().getMinPercentOfReplicas(), 100);
+
+ selector.onInstancesChange(ImmutableSet.of(REPLICA_INSTANCE_0,
REPLICA_INSTANCE_1),
+ List.of(REPLICA_INSTANCE_2));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 66);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test
+ public void testReplicaHealthReflectsStrictReplicaGroupKnockout() {
+ // segment0 is missing on instance2 while segment1 is fully loaded. Strict
replica group routing takes
+ // instance2 out of service for every segment sharing that ideal state
assignment, so segment1 becomes
+ // under-replicated too even though its external view says otherwise. This
is the degradation a metric
+ // computed from the external view alone cannot see.
+ createOldSegments(List.of("segment0"));
+ Map<String, List<Pair<String, String>>> idealStateAssignment =
+ Map.of("segment0", allOnline(), "segment1", allOnline());
+ Map<String, List<Pair<String, String>>> externalViewAssignment =
+ Map.of("segment0", partiallyOnline(2), "segment1", allOnline());
+
+ BaseInstanceSelector strictSelector =
+
createReplicaHealthSelector(STRICT_REPLICA_GROUP_INSTANCE_SELECTOR_TYPE,
REPLICA_INSTANCES,
+ idealStateAssignment, externalViewAssignment);
+ SegmentReplicaHealth strictReplicaHealth =
strictSelector.getReplicaHealth();
+ assertEquals(strictReplicaHealth.getMinPercentOfReplicas(), 66);
+
+ // Without the strict guarantee only the segment that is actually missing
a replica is affected
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector balancedSelector =
+ createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR,
REPLICA_INSTANCES, idealStateAssignment,
+ externalViewAssignment);
+ SegmentReplicaHealth balancedReplicaHealth =
balancedSelector.getReplicaHealth();
+ assertEquals(balancedReplicaHealth.getMinPercentOfReplicas(), 66);
Review Comment:
If both are 66% , what's the point of this test?
##########
pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java:
##########
@@ -1927,4 +1934,511 @@ public void testReplicaGroupAdaptiveServerSelector() {
assertEquals(selectedResult.getLeft(), expectedSelection);
}
+
+ // Replica health metrics
+ //
+ // The scenarios below all use the same three instances and assert on the
SegmentReplicaHealth the
+ // selector derives, since that is what the gauges are emitted from. Each
segment's percentage is
+ // measured against the replicas its own ideal state assigns, so segments do
not have to be uniformly
+ // replicated for the numbers to make sense.
+
+ private static final String REPLICA_INSTANCE_0 = "instance0";
+ private static final String REPLICA_INSTANCE_1 = "instance1";
+ private static final String REPLICA_INSTANCE_2 = "instance2";
+ private static final Set<String> REPLICA_INSTANCES =
+ ImmutableSet.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+
+ /// Returns the ideal state assignment placing the segment on all three
instances as ONLINE.
+ private static List<Pair<String, String>> allOnline() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_2, ONLINE));
+ }
+
+ /// Returns an assignment placing the segment on two of the three instances
as ONLINE.
+ private static List<Pair<String, String>> twoReplicas() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE));
+ }
+
+ /// Returns an external view assignment where the first `numOnline` of the
three instances are ONLINE
+ /// and the rest are OFFLINE, so the segment looks partially loaded.
+ private static List<Pair<String, String>> partiallyOnline(int numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : OFFLINE));
+ }
+ return assignment;
+ }
+
+ private BaseInstanceSelector createReplicaHealthSelector(String
selectorType, Set<String> enabledInstances,
+ Map<String, List<Pair<String, String>>> idealStateAssignment,
+ Map<String, List<Pair<String, String>>> externalViewAssignment) {
+ // Sorted so that the order the selector looks up segment metadata in is
deterministic, which is what
+ // the stub set up by createSegmentCreationTimes matches on
+ return (BaseInstanceSelector) createTestInstanceSelector(selectorType,
enabledInstances,
+ createIdealState(idealStateAssignment),
createExternalView(externalViewAssignment),
+ new TreeSet<>(externalViewAssignment.keySet()));
+ }
+
+ /// Stubs the segment metadata lookup with the given creation times, in the
order the selector reads them.
+ private void createSegmentCreationTimes(Map<String, Long>
creationTimeMsBySegment) {
+ List<Pair<String, Long>> creationTimes = new
ArrayList<>(creationTimeMsBySegment.size());
+ for (String segment : new TreeSet<>(creationTimeMsBySegment.keySet())) {
+ creationTimes.add(new ImmutablePair<>(segment,
creationTimeMsBySegment.get(segment)));
+ }
+ createSegments(creationTimes);
+ }
+
+ /// Marks the given segments as created long enough ago that they are no
longer treated as new, so that
+ /// they count towards the replica health even though their external view
has not converged.
+ private void createOldSegments(List<String> segments) {
+ long creationTimeMs = _mutableClock.millis() -
NEW_SEGMENT_EXPIRATION_MILLIS - 1;
+ Map<String, Long> creationTimes = new HashMap<>();
+ for (String segment : segments) {
+ creationTimes.put(segment, creationTimeMs);
+ }
+ createSegmentCreationTimes(creationTimes);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthFullyReplicated(String selectorType) {
+ // Every segment is ONLINE everywhere the ideal state assigns it
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", allOnline(), "segment1", allOnline()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ // Nothing is degraded, so no expected replica count has to be remembered
+ assertTrue(selector._oldSegmentExpectedReplicasMap.isEmpty());
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ /// Returns an external view assignment with the first `numOnline` of the
three instances ONLINE and the
+ /// rest in ERROR, i.e. replicas that failed their state transition rather
than merely being offline.
+ private static List<Pair<String, String>> partiallyOnlineRestInError(int
numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : ERROR));
+ }
+ return assignment;
+ }
+
+ private static List<Pair<String, String>> singleReplica(String state) {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, state));
+ }
+
+ /// Returns an ideal state assignment placing the segment on all three
instances as CONSUMING, i.e. a
+ /// segment the controller still considers in progress.
+ private static List<Pair<String, String>> allConsuming() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, CONSUMING),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING));
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testConsumingSegmentMeasuredLikeAnyOther(String selectorType) {
+ // Consuming segments are deliberately not special-cased. A partition
whose replicas are all gone will also
+ // raise an ingestion alert, and reconciling that overlap belongs in the
alerting pipeline rather than in a
+ // metric that would otherwise stop meaning what its name says.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
partiallyOnline(0)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testHealthyConsumingSegmentReportsFullyReplicated(String
selectorType) {
+ // The flip side of not excluding them: a partition consuming normally on
every replica must read 100%, or
+ // every real-time table would look permanently degraded. CONSUMING counts
as serving for routing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
allConsuming()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testCommittedSegmentCountedWhilePeersStillDownloading(String
selectorType) {
+ // The exclusion has to end at the commit, not when the last replica
finishes downloading. The ideal state
+ // turns ONLINE at commit while peers still report CONSUMING, and that
segment is an ordinary immutable
+ // one whose replicas are genuinely missing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ // Committed: ideal state ONLINE everywhere
+ Map.of("segment0", allOnline()),
+ // Only the committer has it; the peers have dropped out rather than
reporting CONSUMING
+ Map.of("segment0", partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
+ 1));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testCommittingSegmentStillRoutableFromPeersReportingConsuming(String
selectorType) {
+ // The normal commit window: ideal state ONLINE, peers still CONSUMING in
the external view. They remain
+ // routable, so nothing is short of replicas and no clock starts.
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING))));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+
+
+
+
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testWithoutRedundancyIgnoresSingleReplicaSegments(String
selectorType) {
+ // A segment the ideal state assigns one replica has no redundancy to
lose, so it must never be counted -
+ // otherwise a table that is single-replica by design reads as permanently
at risk.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", singleReplica(ONLINE)), Map.of("segment0",
singleReplica(OFFLINE)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ @Test
+ public void testWithoutRedundancyCountsLastReplicaNotMerelyShort() {
+ // The whole point of a percentage: 2 of 3 is 66% and passes, 1 of 3 is
33% and does not. Pinned because
+ // this is the line the controller-side alert drew, and moving it silently
would change what pages.
+ // Balanced routing only: under strict replica groups segment1's gaps
would exclude those groups for
+ // segment0 as well, taking it to 1 of 3 and hiding the threshold this
asserts on.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", partiallyOnline(2), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+
assertEquals(selector.getReplicaHealth().getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentOnItsLastReplica(String
selectorType) {
+ // This is where the count parts company with the alert's percentage. 1 of
2 is 50%, above the threshold,
+ // so the percentage leaves it alone - but the segment genuinely has no
redundancy, and nothing alerts on
+ // the count, so it is reported. Both segments count: segment0 at 1 of 2
and segment1 at 1 of 3.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas(), "segment1", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE)), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 2);
+ // The percentage still reports the worst of the two, which is the 1-of-3
segment
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 33);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentWithNoReplicaLeft(String
selectorType) {
+ // Leaving 1 of 2 alone must not extend to 0 of 2: the data is gone, and
0% is below the threshold like
+ // any other total loss. A replica-count rule keyed on "three or more
assigned" would miss this.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
OFFLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE))));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ long nowMs = _mutableClock.millis();
Review Comment:
Unused?
##########
pinot-broker/src/test/java/org/apache/pinot/broker/routing/instanceselector/InstanceSelectorTest.java:
##########
@@ -1927,4 +1934,511 @@ public void testReplicaGroupAdaptiveServerSelector() {
assertEquals(selectedResult.getLeft(), expectedSelection);
}
+
+ // Replica health metrics
+ //
+ // The scenarios below all use the same three instances and assert on the
SegmentReplicaHealth the
+ // selector derives, since that is what the gauges are emitted from. Each
segment's percentage is
+ // measured against the replicas its own ideal state assigns, so segments do
not have to be uniformly
+ // replicated for the numbers to make sense.
+
+ private static final String REPLICA_INSTANCE_0 = "instance0";
+ private static final String REPLICA_INSTANCE_1 = "instance1";
+ private static final String REPLICA_INSTANCE_2 = "instance2";
+ private static final Set<String> REPLICA_INSTANCES =
+ ImmutableSet.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+
+ /// Returns the ideal state assignment placing the segment on all three
instances as ONLINE.
+ private static List<Pair<String, String>> allOnline() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_2, ONLINE));
+ }
+
+ /// Returns an assignment placing the segment on two of the three instances
as ONLINE.
+ private static List<Pair<String, String>> twoReplicas() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, ONLINE), new
ImmutablePair<>(REPLICA_INSTANCE_1, ONLINE));
+ }
+
+ /// Returns an external view assignment where the first `numOnline` of the
three instances are ONLINE
+ /// and the rest are OFFLINE, so the segment looks partially loaded.
+ private static List<Pair<String, String>> partiallyOnline(int numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : OFFLINE));
+ }
+ return assignment;
+ }
+
+ private BaseInstanceSelector createReplicaHealthSelector(String
selectorType, Set<String> enabledInstances,
+ Map<String, List<Pair<String, String>>> idealStateAssignment,
+ Map<String, List<Pair<String, String>>> externalViewAssignment) {
+ // Sorted so that the order the selector looks up segment metadata in is
deterministic, which is what
+ // the stub set up by createSegmentCreationTimes matches on
+ return (BaseInstanceSelector) createTestInstanceSelector(selectorType,
enabledInstances,
+ createIdealState(idealStateAssignment),
createExternalView(externalViewAssignment),
+ new TreeSet<>(externalViewAssignment.keySet()));
+ }
+
+ /// Stubs the segment metadata lookup with the given creation times, in the
order the selector reads them.
+ private void createSegmentCreationTimes(Map<String, Long>
creationTimeMsBySegment) {
+ List<Pair<String, Long>> creationTimes = new
ArrayList<>(creationTimeMsBySegment.size());
+ for (String segment : new TreeSet<>(creationTimeMsBySegment.keySet())) {
+ creationTimes.add(new ImmutablePair<>(segment,
creationTimeMsBySegment.get(segment)));
+ }
+ createSegments(creationTimes);
+ }
+
+ /// Marks the given segments as created long enough ago that they are no
longer treated as new, so that
+ /// they count towards the replica health even though their external view
has not converged.
+ private void createOldSegments(List<String> segments) {
+ long creationTimeMs = _mutableClock.millis() -
NEW_SEGMENT_EXPIRATION_MILLIS - 1;
+ Map<String, Long> creationTimes = new HashMap<>();
+ for (String segment : segments) {
+ creationTimes.put(segment, creationTimeMs);
+ }
+ createSegmentCreationTimes(creationTimes);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthFullyReplicated(String selectorType) {
+ // Every segment is ONLINE everywhere the ideal state assigns it
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", allOnline(), "segment1", allOnline()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ // Nothing is degraded, so no expected replica count has to be remembered
+ assertTrue(selector._oldSegmentExpectedReplicasMap.isEmpty());
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ /// Returns an external view assignment with the first `numOnline` of the
three instances ONLINE and the
+ /// rest in ERROR, i.e. replicas that failed their state transition rather
than merely being offline.
+ private static List<Pair<String, String>> partiallyOnlineRestInError(int
numOnline) {
+ List<String> instances = List.of(REPLICA_INSTANCE_0, REPLICA_INSTANCE_1,
REPLICA_INSTANCE_2);
+ List<Pair<String, String>> assignment = new ArrayList<>(instances.size());
+ for (int i = 0; i < instances.size(); i++) {
+ assignment.add(new ImmutablePair<>(instances.get(i), i < numOnline ?
ONLINE : ERROR));
+ }
+ return assignment;
+ }
+
+ private static List<Pair<String, String>> singleReplica(String state) {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, state));
+ }
+
+ /// Returns an ideal state assignment placing the segment on all three
instances as CONSUMING, i.e. a
+ /// segment the controller still considers in progress.
+ private static List<Pair<String, String>> allConsuming() {
+ return List.of(new ImmutablePair<>(REPLICA_INSTANCE_0, CONSUMING),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING));
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testConsumingSegmentMeasuredLikeAnyOther(String selectorType) {
+ // Consuming segments are deliberately not special-cased. A partition
whose replicas are all gone will also
+ // raise an ingestion alert, and reconciling that overlap belongs in the
alerting pipeline rather than in a
+ // metric that would otherwise stop meaning what its name says.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
partiallyOnline(0)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testHealthyConsumingSegmentReportsFullyReplicated(String
selectorType) {
+ // The flip side of not excluding them: a partition consuming normally on
every replica must read 100%, or
+ // every real-time table would look permanently degraded. CONSUMING counts
as serving for routing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allConsuming()), Map.of("segment0",
allConsuming()));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testCommittedSegmentCountedWhilePeersStillDownloading(String
selectorType) {
+ // The exclusion has to end at the commit, not when the last replica
finishes downloading. The ideal state
+ // turns ONLINE at commit while peers still report CONSUMING, and that
segment is an ordinary immutable
+ // one whose replicas are genuinely missing.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ // Committed: ideal state ONLINE everywhere
+ Map.of("segment0", allOnline()),
+ // Only the committer has it; the peers have dropped out rather than
reporting CONSUMING
+ Map.of("segment0", partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
+ 1));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testCommittingSegmentStillRoutableFromPeersReportingConsuming(String
selectorType) {
+ // The normal commit window: ideal state ONLINE, peers still CONSUMING in
the external view. They remain
+ // routable, so nothing is short of replicas and no clock starts.
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, CONSUMING), new
ImmutablePair<>(REPLICA_INSTANCE_2, CONSUMING))));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 100);
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+
+
+
+
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testWithoutRedundancyIgnoresSingleReplicaSegments(String
selectorType) {
+ // A segment the ideal state assigns one replica has no redundancy to
lose, so it must never be counted -
+ // otherwise a table that is single-replica by design reads as permanently
at risk.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", singleReplica(ONLINE)), Map.of("segment0",
singleReplica(OFFLINE)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 0);
+ }
+
+ @Test
+ public void testWithoutRedundancyCountsLastReplicaNotMerelyShort() {
+ // The whole point of a percentage: 2 of 3 is 66% and passes, 1 of 3 is
33% and does not. Pinned because
+ // this is the line the controller-side alert drew, and moving it silently
would change what pages.
+ // Balanced routing only: under strict replica groups segment1's gaps
would exclude those groups for
+ // segment0 as well, taking it to 1 of 3 and hiding the threshold this
asserts on.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("segment0", allOnline(), "segment1", allOnline()),
+ Map.of("segment0", partiallyOnline(2), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+
assertEquals(selector.getReplicaHealth().getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentOnItsLastReplica(String
selectorType) {
+ // This is where the count parts company with the alert's percentage. 1 of
2 is 50%, above the threshold,
+ // so the percentage leaves it alone - but the segment genuinely has no
redundancy, and nothing alerts on
+ // the count, so it is reported. Both segments count: segment0 at 1 of 2
and segment1 at 1 of 3.
+ createOldSegments(List.of("segment0", "segment1"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas(), "segment1", allOnline()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
ONLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE)), "segment1",
partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 2);
+ // The percentage still reports the worst of the two, which is the 1-of-3
segment
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 33);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void
testWithoutRedundancyCountsTwoReplicaSegmentWithNoReplicaLeft(String
selectorType) {
+ // Leaving 1 of 2 alone must not extend to 0 of 2: the data is gone, and
0% is below the threshold like
+ // any other total loss. A replica-count rule keyed on "three or more
assigned" would miss this.
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", twoReplicas()),
+ Map.of("segment0", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
OFFLINE),
+ new ImmutablePair<>(REPLICA_INSTANCE_1, OFFLINE))));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ long nowMs = _mutableClock.millis();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+ @Test
+ public void testWithoutRedundancyWatchesReplicatedPartOfMixedTable() {
+ // A real-time table whose consuming segment lives on one replica while
its completed segments live on
+ // three. Losing two replicas of the completed segment has to be reported,
and the thinly replicated
+ // consuming segment must not stop that from happening.
+ createOldSegments(List.of("completed"));
+ BaseInstanceSelector selector =
createReplicaHealthSelector(BALANCED_INSTANCE_SELECTOR, REPLICA_INSTANCES,
+ Map.of("consuming", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
CONSUMING)), "completed", allOnline()),
+ Map.of("consuming", List.of(new ImmutablePair<>(REPLICA_INSTANCE_0,
CONSUMING)), "completed",
+ partiallyOnline(1)));
+
+ _mutableClock.fastForward(Duration.ofMillis(NEW_SEGMENT_EXPIRATION_MILLIS
* 2));
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getNumSegmentsWithoutRedundancy(), 1);
+ }
+
+
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthPartiallyReplicated(String selectorType) {
+ // segment0 is only loaded on 1 of its 3 replicas, which is the threshold
the low replica alert fires on
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0",
partiallyOnline(1)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 33);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 0);
+ }
+
+ @Test(dataProvider = "selectorType")
+ public void testReplicaHealthUnavailableSegment(String selectorType) {
+ // segment0 is loaded nowhere
+ createOldSegments(List.of("segment0"));
+ BaseInstanceSelector selector = createReplicaHealthSelector(selectorType,
REPLICA_INSTANCES,
+ Map.of("segment0", allOnline()), Map.of("segment0",
partiallyOnline(0)));
+
+ SegmentReplicaHealth replicaHealth = selector.getReplicaHealth();
+ assertEquals(replicaHealth.getMinPercentOfReplicas(), 0);
+ assertEquals(replicaHealth.getNumUnavailableSegments(), 1);
+ // An unavailable segment is not also counted as under-replicated
Review Comment:
+1, this comment is inaccurate
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]