xiangfu0 opened a new pull request, #20022:
URL: https://github.com/apache/pinot/pull/20022

   Stacked on #20021 (the first two commits are that PR; this PR is the last 
commit). Second of the DATA-3342 series; see #20021 for the incident write-up.
   
   ## Problem
   
   Once a broker-to-server channel is half-open, the failure detector cannot 
get the server off it:
   
   1. **The retry probe never saw the channel that carries the queries.** 
`QueryRouter#hasChannel` and `QueryRouter#connect` hard-code 
`TableType.OFFLINE`, and `ServerRoutingInstance#equals` includes the table 
type, so on a realtime-only cluster the single-stage retrier found no channel, 
answered `UNKNOWN`, and `BaseExponentialBackoffRetryFailureDetector` (which 
treats a round with no `UNHEALTHY` vote as recovered) readmitted the server 
onto whatever channel was cached. In the 2026-09-05 incident the health mark 
came 84 s after Helix readmission and changed nothing, because the probe had 
never looked at the REALTIME channel.
   2. **The probe could not fail on a half-open channel anyway.** It only 
reconnects when the cached channel is `null` or `!isActive()`; a half-open 
socket reports active until the kernel gives up on retransmits.
   3. **Timeouts never reached the detector.** `AsyncQueryResponse` only sets 
`_failedServer` on a send failure or a channel-inactive event. A query that 
timed out because the server never answered reported nothing, so a channel that 
swallowed 1.07M queries over 37 minutes (2026-10-09) never produced a single 
unhealthy mark.
   
   ## Fix
   
   **Probe every channel the server was queried over.** 
`QueryRouter#hasChannel` answers whether the broker has sent a query to the 
server over any of its channels (OFFLINE or REALTIME, TLS or plaintext), and 
`QueryRouter#connect` re-establishes each such channel through the new 
`ServerChannels#probe`: a closed channel is reconnected as before, and a 
channel marked suspect is replaced with a fresh connection instead of being 
trusted on `isActive()`. Channels that only startup pre-connect opened are 
still left alone, so a multi-stage-only cluster keeps answering `UNKNOWN`.
   
   **Report a server after consecutive unanswered timeouts.** `QueryRouter` 
keeps, per channel, the number of queries that timed out without the server 
answering anything since. Any response from the server resets it, even a late 
one for a query that already timed out, so a slow server that still answers 
some queries never reaches the threshold; a dead channel answers nothing. When 
a query times out, `AsyncQueryResponse` records every server the request 
reached but that never answered; the first one to cross the threshold becomes 
the failed server, the request handler reports it to the failure detector 
(which excludes it from routing), and its channel is marked suspect so the 
probe replaces it. New broker config key 
`pinot.broker.failure.detector.consecutive.timeouts.threshold`, default 5, 
non-positive disables. A timeout stays a timeout: no send exception is set, the 
query response is unchanged.
   
   **All-UNKNOWN is recovered, deliberately.** The detector keeps treating a 
retry round where every retrier answers `UNKNOWN` as recovered, now with a log 
line saying so. After an eviction (readmission in #20021, or 
`DirectOOMHandler`) no transport holds a channel to the server, so there is 
nothing to verify; keeping it unhealthy would leave it out of routing for the 
rest of the backoff schedule (about 85 minutes with the defaults), and because 
`markServerUnhealthy` is `computeIfAbsent`, a later failure could not flag it 
again. With the probe fix, `UNKNOWN` is rare and honest.
   
   ### How the incidents play out with this change
   
   - Realtime-only cluster, dead channel after a node flap: the first five 
unanswered queries report the server; it leaves routing; five seconds later the 
probe replaces the REALTIME channel with a fresh connection to the restarted 
process and the server is readmitted onto a working channel. Previously the 
probe answered `UNKNOWN` and readmitted it onto the dead one.
   - Path stall on three connections: each stalled channel's server is reported 
after five unanswered queries (within one query timeout of the stall at that 
query rate), leaves routing, and is probed over a fresh connection, which takes 
a different path or fails loudly. Queries stop being routed into the stall 
within seconds instead of 37 minutes.
   
   ## Compatibility
   
   No wire format or serialization change. `QueryRouter#hasChannel`/`connect` 
keep their signatures; the old five-argument `QueryRouter` constructor remains. 
The one behaviour change for the detector is that the single-stage retrier now 
votes on realtime-only clusters, which is the fix.
   
   ## Tests
   
   - `QueryRoutingTest` against a real `QueryServer`:
     - `testRetryProbeCoversRealtimeOnlyChannel`: after a REALTIME-only query 
`hasChannel` is true, the probe succeeds and keeps the healthy channel.
     - `testConsecutiveUnansweredTimeoutsReportServerAndProbeReplacesChannel`: 
with a server that answers nothing (threshold 3, 300 ms timeout) the third 
timeout reports the REALTIME routing instance with no send exception, the count 
resets, and the probe closes the suspect channel and opens a fresh one that the 
next probe keeps.
     - `testServerResponseResetsConsecutiveTimeouts`: a late answer to an 
already timed-out query resets the count, so the next timeouts start over.
   - `AsyncQueryResponseTest`: a timeout reports the server the router flags 
and never a server the request did not reach; below the threshold nothing is 
reported; an answered query records no timeout.
   - `ServerChannelsTest#testProbeReplacesSuspectChannelAndKeepsHealthyOne`: 
pre-connect-only channels are not probed; a healthy channel is kept, a suspect 
one replaced, a closed one reconnected, an unreachable server fails the probe 
and leaves no channel behind.
   - 
`ConnectionFailureDetectorTest#testAllRetriersUnknownIsTreatedAsRecovered`: 
pins the all-UNKNOWN semantics and that the server can be flagged again 
afterwards.
   
   Verified locally with the transport and broker routing suites touched by 
#20021 plus the above, and checkstyle and license checks on `pinot-spi`, 
`pinot-common`, `pinot-core`, `pinot-broker`.
   
   ## Follow-ups (DATA-3342)
   
   - Cancel queued writes for queries that already timed out, and have the 
server drop requests whose broker deadline has passed (`InstanceRequestHandler` 
sets the arrival time at socket read), so a recovered channel does not execute 
a stale backlog.
   - Evict channels on a `LiveInstance` session change, for pod restarts that 
leave the instance enabled in Helix.
   


-- 
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]

Reply via email to