Skip to content

Upstream/fail open sse - #19908

Draft
kpatel006 wants to merge 3 commits into
apache:masterfrom
kpatel006:upstream/fail-open-sse
Draft

kpatel006 wants to merge 3 commits into
apache:masterfrom
kpatel006:upstream/fail-open-sse

Conversation

@kpatel006

@kpatel006 kpatel006 commented Oct 16, 2026 •

Copy link
Copy Markdown

PR flow

Successful fail-open routing flow when all unavailable segments have eligible unhealthy replicas.

flowchart TD
  N0["Normal selection over routable servers #40;F9#41;"]:::stModified
  N1["Unavailable segments present #40;F9#41;"]:::stModified
  N2["Fail#45;open enabled and not multi#45;stage #40;F9#41;"]:::stModified
  N3["At least one segment has fail#45;open candidates #40;F9#41;"]:::stModified
  N4["Emit FAIL#95;OPEN#95;ELIGIBLE#95;REQUESTS #40;F9#41;"]:::stModified
  N5["Attempt fail#45;open selection #40;F9#41;"]:::stModified
  N6["Get unhealthy servers from failure detector #40;F9#41;"]:::stModified
  N7["For each segment#44; get fail#45;open candidates and filter to unhealthy #40;F9#41;"]:::stModified
  N8["Select fail#45;open candidates #40;using selector#39;s logic#41; #40;F9#41;"]:::stModified
  N9["Fail#45;open selection covers all segments #40;F9#41;"]:::stModified
  N10["Merge normal and fail#45;open mappings #40;F9#41;"]:::stModified
  N11["Return complete mapping #40;no unavailable segments#41; #40;F9#41;"]:::stModified
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4
  N4 --> N5
  N5 --> N6
  N6 --> N7
  N7 --> N8
  N8 --> N9
  N9 --> N10
  N10 --> N11
  classDef stAdded fill:#dafbe1,stroke:#1a7f37,color:#1f2328,stroke-width:2px
  classDef stModified fill:#fff8c5,stroke:#9a6700,color:#1f2328,stroke-width:2px
  classDef stRemoved fill:#ffebe9,stroke:#cf222e,color:#1f2328,stroke-width:2px
  classDef stUnchanged fill:#f6f8fa,stroke:#656d76,color:#1f2328,stroke-width:1px
Loading

AI-generated · Green: added · Yellow: modified · Red: removed · Gray: existing

Partial evidence: 0 file patches omitted; 1 truncated.

Diff evidence
  • F9: pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java — before · after
  • Regenerate PR flow

Background

The broker has two separate ideas of which servers it can use:

  • Enabled servers (_enabledServerInstanceMap): servers Helix marks as enabled, i.e. not disabled by an operator.
  • Routable servers (_routableServerInstanceMap): enabled servers minus the ones the FailureDetector has
    excluded after connection or query failures.

Instance selectors only ever see the routable set. If every replica of a segment is excluded, the segment counts as
unavailable. The query then returns partial results and an unavailable-segments error.

Motivation

The failure detector can be wrong or too aggressive: a network blip, a slow GC, or a burst of timeouts across replicas.
When that hits every replica of a segment, the broker drops data it could probably have served, which is a
self-inflicted partial outage. Fail-open routing makes the broker try an unhealthy replica as a last resort instead
of skipping the segment.

It never routes to a server an operator has disabled. It also never overrides a healthy routing choice.

Summary of changes

  • When every replica of a segment is excluded by the failure detector, the broker routes to an unhealthy replica that
    an operator hasn't disabled.
  • It's all-or-nothing: if any unavailable segment has no such replica, you get the normal unavailable-segments result.
  • Healthy picks are never overridden. Multi-stage queries are excluded for now (see Follow-up).
  • The balanced and replica-group selectors get their own fail-open selection logic.
  • A successful fail-open query doesn't mark the server healthy. Recovery is still up to the FailureDetector.
  • Multi-cluster brokers now forward exclude/include notifications to remote-cluster routing managers. The retry path can
    also find servers that exist only in a remote cluster.

This also fixes an existing gap in multi-cluster brokers, whether or not fail-open is enabled. On master, the
failure detector only notifies the local routing manager, so remote-cluster servers it flags are never excluded from
routing. The retry path also can't find those servers, so they stay "unhealthy" until max retries force them healthy.
With this change, remote-cluster routing managers exclude and re-include servers the same way the local one does.

Follow-up

A separate PR will add fail-open support for multi-stage (MSE) queries, StrictReplicaGroupInstanceSelector, and
MultiStageReplicaGroupSelector.

Diagrams

Request-time selection flow

flowchart TD
    A["Query arrives<br/>BaseInstanceSelector.select()"] --> B["Normal select()<br/>over routable servers"]
    B --> C{"Any requested segments<br/>unavailable?"}
    C -- No --> R1["Return normal mapping"]
    C -- Yes --> D{"Any of them have<br/>enabled-but-excluded replicas?"}
    D -- No --> R2["Return mapping +<br/>unavailable segments"]
    D -- Yes --> M["Emit FAIL_OPEN_ELIGIBLE_REQUESTS"]
    M --> E{"FailOpenPolicy enabled<br/>AND not multi-stage?"}
    E -- No --> R2
    E -- Yes --> F["For each unavailable segment:<br/>enabled replicas ∩ detector.getUnhealthyServers()"]
    F --> G{"Every segment has<br/>≥1 candidate?"}
    G -- No --> R2
    G -- Yes --> H["selectForFailOpen()<br/>Balanced: round-robin by requestId<br/>ReplicaGroup: fixed-replica off"]
    H --> I{"Full coverage?"}
    I -- No --> R2
    I -- Yes --> J["Merge: healthy mapping + fail-open picks<br/>record POOL_SEG_QUERIES"]
    J --> R3["Return complete mapping,<br/>no unavailable segments"]
Loading

Component wiring and state propagation

flowchart LR
    subgraph Config
        BC["Broker conf<br/>pinot.broker.enable.fail.open"]
        CC["Cluster config override<br/>(dynamic)"]
    end
    BC --> FOP["FailOpenPolicy<br/>volatile enabled flag"]
    CC -- "onChange()<br/>delete reverts to broker value" --> FOP

    FD["FailureDetector"] -- "exclude/include server" --> BS["BaseBrokerStarter /<br/>MultiClusterHelixBrokerStarter"]
    BS --> LRM["BrokerRoutingManager (local)"]
    BS -- "new: forwarded" --> RRM["RemoteClusterBrokerRoutingManager(s)"]

    FOP --> LRM
    FD --> LRM
    FOP --> RRM
    FD --> RRM

    LRM -- "onInstancesChange(routable,<br/>failOpenEligible = all enabled, changed)" --> IS["InstanceSelector<br/>(+ sampler selectors)"]
    RRM --> IS
    IS -- "rebuild SegmentStates:<br/>failOpenCandidatesMap,<br/>segmentsWithoutCandidates" --> SS["SegmentStates"]
    SS -- "FAIL_OPEN_ELIGIBLE_SEGMENTS" --> MET["BrokerMetrics"]

    RH["Request handlers<br/>(gRPC / SingleConn / MSE)"] -- "retryUnhealthyServer →<br/>getServerInstanceForRetry<br/>local, then multi-cluster" --> FD
Loading

Tests added

  • InstanceSelectorTest: about 17 tests run per selector type. They cover the policy being disabled, the
    multi-stage gate, the all-or-nothing rule, rejection of disabled or unassigned servers, keeping healthy picks,
    deterministic choice by requestId, the replica-group behaviors, and pool metrics.
  • BrokerRoutingManagerTest: fail-open picks become ordinary routing table entries, for both the main and sampler
    selectors, and excluded servers stay eligible after an instance config change.
  • BaseBrokerRequestHandlerTest: covers the fallback order in getServerInstanceForRetry.
  • MultiClusterHelixBrokerStarterTest: checks that detector notifications reach the remote routing managers,
    including notifications that arrive before those managers exist.

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough review across correctness, mixed-version compatibility, concurrency/distributed state, and security. This is well-structured work — the all-or-nothing coverage, the healthy-mapping-never-overridden guarantee, the admin-disabled exclusion, and keeping recovery owned by the failure detector are all sound, and the test coverage is genuinely thorough.

I couldn't find any correctness, compatibility, or concurrency defect: the global write lock serializes the exclude/include/instance-config paths so the fail-open-eligible snapshots are consistent, the new InstanceSelector default methods preserve behavior for the MSE/strict selectors, old constructors are all retained, and nothing touches a wire or serialized format. The snapshot-vs-live-unhealthy-set window degrades safely (falls back to reporting unavailable).

Leaving this as a comment since the PR is still a draft. Only a couple of minor nits inline — nothing blocking.

public static final BrokerMeter ROUTING_TABLE_UPDATE_FAILURE =
create("ROUTING_TABLE_UPDATE_FAILURE", "failures", false);

// Panic routing metrics

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small naming nit: this comment says "Panic routing metrics", but the feature, config key, and meter names all use "fail-open" everywhere else. Looks like leftover naming from an earlier iteration — worth renaming to "Fail-open routing metrics" so operators grepping for the feature find it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its based off another PR, which has this removed: #19891. Once its merged, I'll rebase and it should go away

for (String segment : unavailableSegments) {
List<SegmentInstanceCandidate> candidates = failOpenSegmentStates.getCandidates(segment);
if (candidates != null && !candidates.isEmpty()) {
SegmentInstanceCandidate candidate = candidates.get(requestId++ % candidates.size());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor defensive nit, not a live bug: this uses plain % on requestId, while ReplicaGroupInstanceSelector.selectServers uses Math.floorMod(...) for the same index. The request id generator is always non-negative today so % is safe, but matching floorMod here (and at the pre-existing line 95) would make it robust if that ever changes.

@Jackie-Jiang Jackie-Jiang added the feature New functionality label Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants