kpatel006 opened a new pull request, #19980:
URL: https://github.com/apache/pinot/pull/19980
## Background
The earlier commits in this stack added a configurable fail-open routing
policy. When every enabled replica of a segment is unavailable, the broker can
route that segment to a replica that the failure detector has marked unhealthy,
instead of reporting the segment as unavailable. The single-stage commit wired
this into single-stage selection, with two limits:
- **MSE was gated off.** `BaseInstanceSelector.select()` turned fail-open
off when `useMultistageEngine` was set (`// TODO: Remove the multi-stage query
gate…`).
- **Fail-open only patched the unavailable segments.** It reselected just
those segments and merged the result into the normal mapping. That works for
balanced and replica-group selectors. It doesn't work for
`MultiStageReplicaGroupSelector`, which must keep every segment of an instance
partition on **one replica group** so colocated joins stay correct. A patch can
put segment A on RG0 and segment B on RG1. The broker picks this selector from
the table's routing config, not the query engine, so the problem affects both
single-stage and multi-stage queries on those tables.
## Summary
### Removing the MSE gate
- Removes the `isUseMultistageEngine` check from
`BaseInstanceSelector.select()`, so fail-open now applies to multi-stage
queries.
### Fail-open reselection changes (all selectors)
- **Full reselection.** `selectFailOpen()` now reruns selection over the
**full per-query segment list** in its original order, not just the unavailable
subset, and returns that mapping as is. Each segment's candidates:
- Healthy segments keep their normal candidates.
- Unavailable segments get their fail-open (unhealthy) candidates.
- Healthy segments that also have unhealthy replicas get those as
**fallback candidates** in a separate map. A selector may use them only as a
last resort.
- **New hooks for subclasses:**
- `shouldTrackFailOpenCandidatesForHealthySegments()`: default `false`;
`true` for Strict and `MultiStageReplicaGroupSelector`.
- `shouldFailOpenSegmentsWithoutHealthyCandidates()`: default `true`;
`false` for `MultiStageReplicaGroupSelector`.
- `canSkipSegmentForFailOpen()`: defaults to the new
`SegmentStates.isNewSegment()`; `MultiStageReplicaGroupSelector` uses
`isNewLLCSegment`.
- **`SegmentStates` tracks new segments** as part of each snapshot, so skip
decisions stay consistent if an assignment refresh races the query.
### Observability
- **Pool metrics in one place.** `POOL_SEG_QUERIES` is now recorded once per
query by `BaseInstanceSelector.recordPoolMetrics()`, from whichever mapping is
actually returned (normal or fail-open). The per-selector recording in
`BalancedInstanceSelector` and `ReplicaGroupInstanceSelector` is removed, which
fixes double counting during fail-open.
- **New meters:** `FAIL_OPEN_ROUTING_SUCCESSES` and
`FAIL_OPEN_ROUTING_FAILURES`.
- **Failure log:** a warning when fail-open fails, rate-limited to 1/s
across all tables and listing at most 5 sample segments.
### Selector changes
- **`BalancedInstanceSelector`:** fail-open now goes through the normal
selection code with adaptive selection turned off, instead of its own simple
round-robin loop. The round-robin sequence continues across fail-open.
- **`ReplicaGroupInstanceSelector`:** fail-open no longer forces
`useFixedReplica=false`, so sticky routing now carries through fail-open.
Adaptive ranking is still bypassed, and the `numReplicaGroupsToQuery` sequence
continues across fail-open.
- **`StrictReplicaGroupInstanceSelector`:** opts into tracking fail-open
candidates for healthy segments. New segments of an unavailable partition now
follow their older siblings onto the same fail-open instance instead of being
skipped.
- **`MultiStageReplicaGroupSelector`:**
- **Two passes in `assign()`.** It first tries the healthy-only map.
During fail-open reselection only, if no replica group can serve a partition,
it retries with the expanded map that includes fallback candidates. Within the
chosen replica group it still prefers healthy instances.
- **Defer instead of throw.** In the normal pass, a partition that can't
be assigned is deferred to fail-open instead of throwing, but only if it
contains an unavailable segment that has fail-open candidates. Otherwise it
throws as before.
- **Fail loudly.** A `select(BrokerRequest, …)` override throws if a
deferred segment ends up neither assigned nor reported unavailable, so a
partition is never silently served with segments missing.
- **Consistent snapshot.** A ThreadLocal hands the `InstancePartitions`
snapshot from normal selection to the fail-open pass, so both passes use the
same partitions. The ThreadLocals are cleared in `finally` on every exit path.
- **Sticky routing on both passes.** `resolvePreferredReplicaGroup()` is
shared, so fixed-replica/sticky routing is honored identically on the normal
and fail-open paths.
## Impact on single-stage queries
This PR is **not** MSE-only. It changes single-stage routing in these ways:
1. **Fail-open reselection works differently for every selector.**
Previously, `selectFailOpen()` reselected only the unavailable segments and
merged them into the normal mapping. Now it reselects the full per-query
segment list and returns that mapping as is.
- **Effect:** healthy segments in a single-stage query are routed again
during fail-open. Usually they land in the same place, but not always. With
adaptive selection, fail-open falls back to round-robin. With
`numReplicaGroupsToQuery`, the newly routed unavailable segments shift the
replica offset. Either case can move a healthy segment to a different *healthy*
server.
2. **Fixed replica is preserved.** `ReplicaGroupInstanceSelector` no longer
overrides `useFixedReplica=false` during fail-open. The test
`testReplicaGroupFailOpenOverridesFixedReplicaQueryOption` was replaced by
`testReplicaGroupFailOpenPreservesFixedReplicaQueryOption`.
3. **Balanced and Strict selectors behave differently during fail-open:**
- **Balanced:** `BalancedInstanceSelector` now uses its normal selection
code, with adaptive selection turned off.
- **Strict:** `StrictReplicaGroupInstanceSelector` routes new sibling
segments of an unavailable partition instead of skipping them.
4. **Pool metrics changed for all queries, including healthy ones.**
`POOL_SEG_QUERIES` is now recorded centrally on every path.
- **Healthy path:** counts should match the old ones.
- **Fail-open path:** the old code counted only the fail-open segments.
The new code counts the whole final mapping, which also fixes double counting.
5. **`MultiStageReplicaGroupSelector` changes also apply to single-stage
queries.** `InstanceSelectorFactory` picks it from the table's routing config
(`MULTI_STAGE_REPLICA_GROUP_SELECTOR_TYPE`), so single-stage queries on those
tables get the new behavior: deferring a partition to fail-open, the fallback
pass, and the new "dropped from the selection" exception.
**When the fail-open policy is disabled:** every reselection change above
sits behind `_failOpenPolicy.isEnabled()`. The only exception is the
pool-metrics refactor, which should be count-equivalent on the healthy path.
## Motivation
- **Close the MSE gap.** MSE queries are the ones that most need a
consistent replica group per partition, and they were the only ones without
fail-open.
- **Correctness first:**
- A partition is never split across replica groups.
- Healthy segments move onto unhealthy servers only when no healthy
assignment exists.
- The broker fails loudly rather than silently dropping segments.
- **Fail-open no longer changes healthy routing, for both engines.**
Reselecting over the full segment list in its original order keeps sticky
routing, round-robin position and pool metrics matching what the normal pass
would have done. The old patch-only approach changed the rotation, overrode
fixed-replica routing, and counted pool metrics twice.
- **Better operational signal:** success/failure meters and a rate-limited
failure log.
## Diagram
```mermaid
flowchart TD
A["BaseInstanceSelector.select(brokerRequest, segments)<br/>(SSE and
MSE; MSE gate removed)"] --> B["select(segments, ...,
failOpenEnabled)<br/>normal pass"]
B --> MSE{"MultiStageReplicaGroupSelector?<br/>(table routing config)"}
MSE -- yes --> M1["Save InstancePartitions snapshot in
ThreadLocal<br/>assign(requireComplete=true)"]
M1 --> M2{"Partition assignable<br/>with healthy servers?"}
M2 -- yes --> C
M2 -- no --> M3{"failOpenEnabled and partition has an<br/>unavailable
segment with fail-open candidates?"}
M3 -- no --> X1["throw: no replica group can serve partition"]
M3 -- yes --> M4["Defer partition's
segments<br/>(_segmentsDeferredToFailOpen)"] --> C
MSE -- "no (Balanced / ReplicaGroup / Strict)" --> C
C{"Unavailable segments<br/>in this request?"}
C -- no --> R1["recordPoolMetrics(normal)<br/>return normal mapping"]
C -- yes --> D{"Fail-open eligible and enabled?"}
D -- no --> R2["recordPoolMetrics(normal)<br/>return normal +
unavailable list"]
D -- yes --> E["selectFailOpen: rebuild states over FULL segment
list<br/>• unavailable → unhealthy candidates<br/>• healthy → normal candidates
+ fallback (unhealthy) list<br/>• new / candidate-less → skip per hooks"]
E --> F["selectForFailOpen()<br/>(adaptive off; sticky/fixed-replica
preserved)"]
F -->
G{"MultiStageReplicaGroupSelector:<br/>assign(requireComplete=false)<br/>healthy
map → fallback map as last resort"}
G --> H{"All required segments assigned?"}
H -- yes -->
S["FAIL_OPEN_ROUTING_SUCCESSES++<br/>recordPoolMetrics(fail-open)<br/>return
fail-open mapping"]
H -- no --> FL["FAIL_OPEN_ROUTING_FAILURES++<br/>rate-limited
WARN<br/>recordPoolMetrics(normal)<br/>return normal mapping + unavailable"]
S --> V
FL --> V{"MultiStageReplicaGroupSelector:<br/>any deferred segment
neither<br/>assigned nor unavailable?"}
V -- yes --> X2["throw: partition dropped,<br/>no consistent replica
group"]
V -- no --> OK["return result<br/>(finally: clear ThreadLocals)"]
```
## Tests added
### `MultiStageReplicaGroupSelectorTest` (22 new tests)
- **Basic fail-open and recovery:**
- `testFailOpenPreservesMultiStageReplicaGroupSelection`
- `testFailOpenReassignsCompletePartitionToFallbackReplicaGroup`
- `testFailOpenRebuildsReplicaGroupWhenHealthyCandidatesAreSplit`
- `testFailOpenRecoversPartitionWithUnavailableSegmentAndHealthySplit`
- **Last-resort fallback rules:**
- `testFailOpenOnlyMovesHealthySegmentsOntoUnhealthyServersAsLastResort`
- `testFailOpenLastResortPrefersHealthyReplicaWithinReplicaGroup`
- `testFailOpenRejectsCandidatesSplitAcrossReplicaGroups`
- **Defer vs. throw:**
- `testFailOpenStillThrowsWhenSplitPartitionHasNoUnavailableSegment`
- `testFailOpenThrowsWhenSplitPartitionFailOpenAlsoFails`
- `testFailedFailOpenReturnsPartialNormalAssignment`
- **New and old segments:**
- `testMseFailOpenSkipsCandidateLessNewLlcSegment`
- `testMseFailOpenDoesNotSkipCandidateLessOldSegment`
- `testFailOpenSkipsNewSegmentWithoutHealthyCandidates`
-
`testFailOpenNewSegmentWithoutHealthyCandidatesDoesNotMoveHealthyPartition`
- **Gating:**
- `testMseFailOpenDisabledForUnavailableSegment`
- `testMseFailOpenNotInvokedForUnavailableSegmentOutsideRequest`
- **Sticky routing:**
- `testFailOpenReselectionHonorsFixedReplicaStickyRouting`
- `testMseFailOpenPreservesFixedReplicaQueryOption`
- **Snapshot and ThreadLocal cleanup:**
- `testSelectForFailOpenUsesInstancePartitionsSnapshotFromPrecedingSelect`
- `testThreadLocalsClearedAfterSuccessfulFailOpen`
- `testThreadLocalClearedAfterHealthySelectionWithFailOpenDisabled`
- `testThreadLocalClearedAfterSelectionThrows`
### `InstanceSelectorTest`
- **Replaced (single-stage behavior changes):**
- `testFailOpenDisabledForMultiStageQuery` →
`testFailOpenEnabledForMultiStageQuery`
- `testReplicaGroupFailOpenOverridesFixedReplicaQueryOption` →
`testReplicaGroupFailOpenPreservesFixedReplicaQueryOption`
- **New, all selector types:**
- `testMultiStageFailOpenSkipsCandidateLessNewLLCSegment`
- `testFailOpenNotInvokedForUnavailableSegmentOutsideRequest`
- `testFailedFailOpenPreservesPartialNormalAssignment`
- **Balanced:**
- `testBalancedFailOpenContinuesRoundRobinSequence`
- `testBalancedFailOpenDoesNotDoubleCountPoolMetrics`
- `testBalancedFailOpenPreservesFixedReplicaQueryOption`
- **Replica group:**
- `testReplicaGroupFailOpenContinuesNumReplicaGroupsToQuerySequence`
- `testReplicaGroupFailOpenNumReplicaGroupsToQueryGreaterThanReplicas`
- **Strict replica group:**
- `testStrictReplicaGroupFailOpenNeverSplitsAcrossMultipleCandidates`
- `testStrictReplicaGroupFailOpenCoversSiblingNewSegment`
- `testStrictReplicaGroupFailOpenKeepsHealthySegmentOnHealthyServer`
-
`testStrictReplicaGroupFailOpenDoesNotMoveHealthyGroupOntoUnhealthyServer`
- `testStrictReplicaGroupFailOpenMatchesIdealWhenAllHealthy`
- `testStrictReplicaGroupFailOpenIsolatedToHealthyPartition`
- `testStrictReplicaGroupFailOpenPreservesFixedReplicaQueryOption`
- `testStrictReplicaGroupFailOpenWithNumReplicaGroupsToQuery`
### `ReplicaGroupSelectorTest`
- `testReplicaGroupFailOpenBypassesAdaptiveRanking`
- `testBalancedFailOpenBypassesAdaptiveRanking`
- `testStrictReplicaGroupFailOpenBypassesAdaptiveWithoutSplittingPartition`
--
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]