Skip to content

Pinot Broker: Add fail-open routing observability metrics - #19891

Open
kpatel006 wants to merge 2 commits into
apache:masterfrom
kpatel006:upstream/fail-open-metrics
Open

kpatel006 wants to merge 2 commits into
apache:masterfrom
kpatel006:upstream/fail-open-metrics

Conversation

@kpatel006

@kpatel006 kpatel006 commented Oct 15, 2026 •

Copy link
Copy Markdown

PR flow

Added fail‑open eligibility metrics: segment state refresh tracks newly ineligible segments; query selection counts requests touching any such segment.

flowchart TD
  N0["BaseInstanceSelector#46;refreshSegmentStates#40;#41; #40;F1#41;"]:::stModified
  N1["Create SegmentStates with segmentsWithoutCandidates #40;F1#44; F2#41;"]:::stModified
  N2["Increment FAIL#95;OPEN#95;ELIGIBLE#95;SEGMENTS for newly ineligible segments #40;F1#44; F3#41;"]:::stModified
  N3["BaseInstanceSelector#46;select#40;#41; #40;F1#41;"]:::stModified
  N4["Check if request touches unavailable#8209;with#8209;candidates segment #40;F1#41;"]:::stModified
  N5["Increment FAIL#95;OPEN#95;ELIGIBLE#95;REQUESTS #40;F1#44; F3#41;"]:::stModified
  N0 -->|"passes"| N1
  N1 -->|"triggers"| N2
  N0 -->|"updates #95;segmentStates used by"| N3
  N3 -->|"calls"| N4
  N4 -->|"if true"| N5
  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

Diff evidence
  • F1: pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/BaseInstanceSelector.java — before · after
  • F2: pinot-broker/src/main/java/org/apache/pinot/broker/routing/instanceselector/SegmentStates.java — before · after
  • F3: pinot-common/src/main/java/org/apache/pinot/common/metrics/BrokerMeter.java — before · after
  • Regenerate PR flow

Background: Panic Routing

Panic routing is when the broker finds no available server for one or more segments in a query. Today the broker gives up on those segments: it marks them unavailable and returns partial results without them.

Fail-open routing handles this differently. If a segment still has candidate servers, but they're all disabled or marked unhealthy, the broker sends the request to one of them anyway. A disabled server often still has the segment loaded and can answer, so serving from it is better than dropping the segment from the result.

This PR doesn't turn on fail-open routing. It adds metrics that show how often panic routing happens and how much of it fail-open could fix.

Will be followed up with implementation PRs

Summary

Adds two broker meters that show how often fail-open routing would help, without changing any routing behavior.

The change splits unavailable segments into two kinds:

Kind Meaning Can fail-open help?
Unavailable, with candidates The segment has replicas, but every candidate instance is disabled Yes: a disabled instance still holds the data
Unavailable, without candidates No instance is hosting the segment at all No: there is nowhere to route

Changes

  • BrokerMeter: two new meters:
    • FAIL_OPEN_ELIGIBLE_REQUESTS (global): counts queries that touch at least one segment that is unavailable but has candidates.
    • FAIL_OPEN_ELIGIBLE_SEGMENTS (per table): counts each time a segment moves into that state. Unchanged refreshes and recoveries don't count.
  • SegmentStates: now also stores which unavailable segments have no candidates.
  • BaseInstanceSelector: records that state during refreshSegmentStates() and updates both meters.
  • InstanceSelectorTest: covers the full lifecycle for every selector type: no candidates → disabled candidate → unchanged refresh → recovery → disabled again.

Motivation

Today every unavailable segment is reported the same way (NO_SERVING_HOST_FOR_SEGMENT), so you can't tell which ones fail-open could actually serve. These meters show how much traffic fail-open would affect, and which tables, before we turn it on.

Diagram

flowchart TD
    subgraph Refresh["refreshSegmentStates()"]
        A[Old segment] --> B{Any enabled<br/>candidates?}
        B -- yes --> C[Routable]
        B -- no --> D{Any candidates<br/>at all?}
        D -- no --> E[Unavailable, without candidates]
        D -- yes --> F[Unavailable, with candidates]
        F --> G{Already in this<br/>state before?}
        G -- no --> H[["FAIL_OPEN_ELIGIBLE_SEGMENTS += 1"]]
        G -- yes --> I[No-op]
    end

    subgraph Select["select() — per query"]
        Q[Query segments] --> R{Touches a segment that's<br/>unavailable but has candidates?}
        R -- yes --> S[["FAIL_OPEN_ELIGIBLE_REQUESTS += 1"]]
        R -- no --> T[No-op]
    end

    Refresh -.->|SegmentStates| Select
Loading

Testing

In e2e testing we saw the metrics being emitted. To achieve this we injected packet loss on the query ports for servers. This caused the brokers to mark the servers unavailable, and over a short period of time all servers were marked as unavailable for segments

The different colors in the following image are different tables that are emitted as containing no server hosts for segments (existing metric)
image

Fail-open eligible segments during the same time period. These are tables who would be considered to be fail-open eligible. A segment is fail-open eligible when none of its replicas are routable, but at least one replica is ONLINE or CONSUMING in both the ideal state and the external view, and sits on a server that the broker's failure detector excluded from routing (as opposed to a server an operator disabled or that is shutting down).
image

Fail-open eligible requests during the same time period:
image

@kpatel006 kpatel006 changed the title Add fail-open routing observability metrics Pinot Broker: Add fail-open routing observability metrics Oct 15, 2026
@kpatel006
kpatel006 marked this pull request as ready for review October 15, 2026 20:13

@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.

Thanks for adding these observability metrics. The fail-open-eligible transition logic looks correct and is nicely covered by testFailOpenEligibleSegmentsMeter. I verified the change compiles (JDK 25) and the global/table flags on each meter match the emit method used. I only have a few minor/nit suggestions inline — no blocking issues.

create("FAIL_OPEN_ELIGIBLE_REQUESTS", "failOpenEligibleRequests", true);

// Tracks segments that become eligible for fail-open routing.
public static final BrokerMeter FAIL_OPEN_ELIGIBLE_SEGMENTS =

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: most recently added meters here carry a /// Javadoc explaining exactly when they are emitted. These two have non-obvious semantics worth documenting the same way: FAIL_OPEN_ELIGIBLE_SEGMENTS is a transition counter (it fires once when a segment enters the unavailable-with-candidates state, not per refresh), while FAIL_OPEN_ELIGIBLE_REQUESTS fires once per query that touches such a segment. Spelling that out will save operators from misreading the rates.

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.

Minor/nit: the phrase "Panic routing" doesn't appear anywhere else in the codebase — the rest of the code calls this "fail-open". Using the same term consistently here would make it easier to grep and reason about.


public SegmentStates(Map<String, List<SegmentInstanceCandidate>> instanceCandidatesMap, Set<String> servingInstances,
Set<String> unavailableSegments) {
this(instanceCandidatesMap, servingInstances, unavailableSegments, unavailableSegments);

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: this public constructor silently defaults segmentsWithoutCandidates to the full unavailableSegments set, which means any SegmentStates built this way treats every unavailable segment as having no candidates (so it never counts as fail-open eligible). That's a safe/conservative default, but it's surprising — a short comment explaining the intent would help future callers, since production now always uses the 4-arg form.

@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.

Thanks for adding these observability metrics. The fail-open-eligible transition logic looks correct and is nicely covered by testFailOpenEligibleSegmentsMeter. I verified the change compiles (JDK 25) and the global/table flags on each meter match the emit method used. I only have a few minor/nit suggestions inline — no blocking issues.

@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.

Approving — only minor/nit comments (doc wording and a clarifying comment on the new constructor default). I verified the change compiles on JDK 25 and the full InstanceSelectorTest suite passes, including the new testFailOpenEligibleSegmentsMeter. The transition-counting logic for FAIL_OPEN_ELIGIBLE_SEGMENTS and the per-request FAIL_OPEN_ELIGIBLE_REQUESTS metric both look correct, and the global/table meter flags match their emit methods.

@codecov-commenter

codecov-commenter commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.43%. Comparing base (c15cead) to head (d8c22d7).
⚠️ Report is 25 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19891      +/-   ##
============================================
+ Coverage     69.26%   69.43%   +0.16%     
  Complexity     1491     1491              
============================================
  Files          3543     3545       +2     
  Lines        233237   233910     +673     
  Branches      37277    37441     +164     
============================================
+ Hits         161560   162410     +850     
+ Misses        59184    58956     -228     
- Partials      12493    12544      +51     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 69.43% <100.00%> (+0.16%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 69.43% <100.00%> (+0.16%) ⬆️
unittests 69.43% <100.00%> (+0.16%) ⬆️
unittests1 58.91% <100.00%> (+0.06%) ⬆️
unittests2 41.22% <100.00%> (+0.28%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kpatel006

Copy link
Copy Markdown
Author

@xiangfu0 I've updated the comments could I get a re-approval?

@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.

Re-reviewed after the latest push (d8c22d7). This revision addresses all three of my earlier minor comments:

  • Both meters now carry /// Javadoc spelling out the emission semantics (FAIL_OPEN_ELIGIBLE_REQUESTS is per-query/global, FAIL_OPEN_ELIGIBLE_SEGMENTS is a transition counter scoped to a table).
  • The inconsistent "Panic routing" comment is gone.
  • The 3-arg SegmentStates constructor now documents its conservative default (treating all unavailable segments as candidate-less).

The routing logic and tests are unchanged from the version I verified earlier. I re-confirmed this revision compiles on JDK 25 and the full InstanceSelectorTest suite passes. No remaining issues — approving.

@kpatel006 kpatel006 mentioned this pull request Oct 16, 2026
1 task
@Jackie-Jiang Jackie-Jiang added metrics Related to metrics emission and collection observability Related to observability (logging, tracing, metrics) labels Oct 16, 2026
@kpatel006

Copy link
Copy Markdown
Author

Thanks for the review! CI is green could someone merge this? I don't have contributor access

@Jackie-Jiang Jackie-Jiang 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.

Reviewed d8c22d7 across the four changed files and the surrounding routing paths. I found four metric-accuracy issues, detailed inline, plus a simplification suggestion: store the fail-open-eligible segments directly so transition detection and request classification use the same explicit state.

All 13 reported CI checks passed for this revision. This was a source review; I did not run local tests.

Comment on lines +450 to +451
if (candidates.isEmpty()) {
segmentsWithoutCandidates.add(segment);

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.

[P2] Distinguish failure-detector exclusions from administrative disables.

A nonempty candidate list does not establish the eligibility described in the PR's testing section. The supplied enabled-instance set excludes servers with HELIX_ENABLED=false, QUERIES_DISABLED=true, or shutdown in progress, as well as failure-detector exclusions. If all replicas are administratively disabled while IS/EV remain ONLINE, both new meters count them as fail-open eligible. Please carry the exclusion reason into this classification and require at least one candidate that is still administratively enabled but excluded by the failure detector. A routing-manager test contrasting these cases would cover the distinction.

Comment on lines +485 to +486
if (!segmentsWithoutCandidates.contains(segment) && !wasAlreadyUnavailableWithCandidates) {
_brokerMetrics.addMeteredTableValue(_tableNameWithType, BrokerMeter.FAIL_OPEN_ELIGIBLE_SEGMENTS, 1);

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.

[P2] Emit segment transitions only from the main table selector.

BaseBrokerRoutingManager creates a separate selector for each configured table sampler, using the same table name and BrokerMetrics, and updates all of them on instance changes. A segment included in N samplers therefore increments this meter N+1 times for one transition. The main routing entry should own table-level emission, following the existing replica-health reporting approach. An overlapping-sampler test would catch this. The transition count can also be accumulated during the existing old-segment loop and emitted once, avoiding the extra pass and repeated meter calls.

Comment on lines +611 to +612
if (failOpenEligible) {
_brokerMetrics.addMeteredGlobalValue(BrokerMeter.FAIL_OPEN_ELIGIBLE_REQUESTS, 1);

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.

[P2] Count eligible requests before subclass selection can throw.

The protected select(...) call at line 596 runs before this increment. For multiStageReplicaGroup, an old segment whose ONLINE IS/EV candidates are all excluded has no enabled candidate list, and MultiStageReplicaGroupSelector.assign() throws before reaching this meter. The segment meter increments, but affected requests are omitted. Classify and record eligibility from the captured snapshot before invoking subclass selection, preserving the existing exception behavior. The new test's selectorType provider does not include this selector.

}
}
if (failOpenEligible) {
_brokerMetrics.addMeteredGlobalValue(BrokerMeter.FAIL_OPEN_ELIGIBLE_REQUESTS, 1);

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.

[P2] Deduplicate request counts across scans of the same physical table.

This increments once per select invocation, while the meter documents once per query per physical table. Both MSE planners route separate leaf scans independently. A non-partitioned self-join with distinct leaves that both touch an eligible segment can therefore increment twice for the same request and physical table. Aggregate eligibility in request-scoped state and emit once per physical table; a self-join regression with distinct leaf filters would cover this.

private final Map<String, List<SegmentInstanceCandidate>> _instanceCandidatesMap;
private final Set<String> _servingInstances;
private final Set<String> _unavailableSegments;
private final Set<String> _segmentsWithoutCandidates;

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.

Simplification: store _failOpenEligibleSegments directly here. Both consumers currently reconstruct eligibility as unavailable minus candidate-less, which introduces compound conditions and negation. An explicit eligible set lets transition detection use !previousEligibleSegments.contains(segment) and request classification use eligibleSegments.contains(segment). The existing three-argument constructor can default it to Set.of(), preserving its conservative behavior. Keep the set in this same published snapshot so routing and eligibility stay consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

metrics Related to metrics emission and collection observability Related to observability (logging, tracing, metrics)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants