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

   ## Problem
   
   Multi-stage (MSE) queries can time out for hours after server pods restart 
together, when every server uses the same mailbox port (for example 8101).
   
   Two server pods can swap IP addresses during a batch restart. A sender whose 
gRPC channel to server A still uses A's old IP then reaches server B. B accepts 
the stream as its own and puts the blocks in a receiving mailbox that nothing 
reads. A waits for the end of stream until the query times out (error 250). The 
sender gets normal acks, so nothing logs an error. The only trace is `Evicting 
dangling receiving mailbox` on B, 300 s later.
   
   Broker dispatch (query server port, for example 8100) has the same exposure: 
B can run a stage plan meant for A.
   
   With random ports this is rare. A restarted server gets a new port, so 
senders use a new channel, and a stale connection hits a closed port.
   
   ## Why a channel keeps the old address
   
   grpc-java 1.81 and 1.84 have the same code here:
   
   - The default load balancer is the old `PickFirstLoadBalancer`.
   - When a connection drops, the subchannel goes to IDLE and resolves the 
hostname once, at that moment. A terminating pod can still be in DNS (for 
example with `publishNotReadyAddresses=true`). `DnsNameResolver` also skips a 
refresh less than 30 s after the last one.
   - The next RPC, maybe much later, connects to that address list. It does not 
resolve again.
   - If another server now listens on that IP and port, the connection works. A 
working connection is never resolved again.
   
   ## Fix
   
   1. **Receivers check who a request is for.**
      - Requests carry a 64-bit hash of the target's instance id in the 
`pinot-target-instance-id-hash` header. The broker adds it to dispatch requests 
(stage plans, cancel, explain). For mailbox streams, plans carry it for each 
receiver (`MailboxInfo.instanceIdHash`, a new proto field), and senders pass it 
on.
      - The query server and the mailbox servers (servers and brokers) reject a 
request for another instance with `FAILED_PRECONDITION` and a 
`pinot-misrouted-to` trailer. A gRPC interceptor does the check after auth, 
before a receiving mailbox is created or a plan runs.
   2. **Senders react to a rejection.**
      - The channel to that address resolves again before the next RPC 
(`ManagedChannel.enterIdle()`). This happens at most once a second per address, 
because a stale connection fails many streams at once.
      - The sending mailbox fails its next data send with 
`SERVER_NOT_RESPONDING` and a clear message.
      - It also sends one error to the intended receiver over a new stream, so 
that the receiver fails now instead of at the deadline. This is best effort: if 
DNS still returns the old address, the receiver waits until the deadline, as 
before.
      - The broker fails the query with `SERVER_NOT_RESPONDING`. It does not 
mark the server unhealthy, because the server is fine.
   3. **Channels resolve again when a server restarts.** This prevents most 
cases before any query fails.
      - Servers refresh the mailbox channel to a peer when the peer finishes 
startup, also when the channel is READY. `ServerGrpcChannelBackoffResetHandler` 
is renamed to `ServerMailboxChannelRefreshHandler`. Before, it only reset the 
backoff of a channel in TRANSIENT_FAILURE.
      - The broker refreshes the dispatch channel to a server each time the 
server becomes routable. Before, it only reset the backoff, and only for 
servers that the failure detector had excluded.
   4. **Channels to old addresses are closed.** When a server moves to another 
address (for example a new random port), servers shut down their mailbox 
channel to the old address, and brokers their dispatch channel, unless another 
server now uses that address. Before, each restart left one unused channel per 
sender.
   5. **Observability.**
      - Three new meters, on servers (`ServerMeter`) and on brokers 
(`BrokerMeter`): `MULTI_STAGE_MISROUTED_REQUESTS_REJECTED`, 
`MULTI_STAGE_MISROUTED_REQUESTS_REJECTED_BY_PEER` and 
`MULTI_STAGE_ORPHANED_RECEIVING_MAILBOXES` (receiving mailboxes that nothing 
read).
      - A receive timeout names the senders that did not finish, for example: 
`Timed out on stage 2 waiting for data from child stage 3 (no end of stream 
from 1 of 46 senders: worker 17 on server-0-1:8101)`.
   
   ## Design notes
   
   - **Instance id, not a per-run id.** I first tried a per-run id 
(incarnation) that each server publishes in its instance config. But brokers 
read it from ZK, and that copy can be stale (unconditional read-modify-write 
updates of instance configs, a ZK restore). Then a healthy server rejects all 
its queries until it restarts. The instance id never changes, so stale metadata 
cannot cause this.
   - **A hash, not the id.** A plan repeats the receivers of a stage for each 
sender worker. An instance id is about as long as a hostname, so it would 
almost double the mailbox part of the plan. The hash adds 9 bytes per receiver 
entry. Sender entries do not carry it. Headers also carry ASCII only, and the 
hex text of a hash is ASCII for any instance id. The hash is FarmHash 
Fingerprint64, which Guava keeps stable across versions.
   - **Refresh, not a new channel.** `enterIdle()` keeps the channel object 
that concurrent senders hold. Streams in flight finish on the old connection. 
It is marked `@ExperimentalApi` in grpc-java.
   - **Not an `:authority` check.** A receiver does not always know the 
hostname that brokers use for it. A false mismatch would reject all traffic.
   - **Keep-alive does not help.** The wrong server answers pings.
   - **TLS.** Hostname verification can block the wrong connection, but not 
when servers share a certificate or use a wildcard one.
   - **One PR.** Closing channels to old addresses and the timeout message are 
not needed for the check. They are here because they come from the same 
incident: the first stops channels from piling up with random ports, and the 
second makes the next lost stream easy to find. I can split them out if 
reviewers prefer.
   
   ## Compatibility
   
   - Receivers accept requests without the new header (older senders). Older 
receivers ignore the header and the new proto field, which is additive. A 
rolling upgrade is safe in any order. Protection starts when the broker, the 
sender and the receiver all run this version.
   - `pinot.query.runner.reject.misrouted.requests` (default `true`) turns off 
the check. Only receivers read it, at startup, so set it on brokers and on 
servers.
   - API changes:
     - `BaseBrokerRoutingManager.setServerReenableCallback` is now 
`setServerEnabledCallback`. It runs for every newly enabled server, not only 
for servers that the failure detector excluded.
     - `QueryDispatcher.resetClientConnectionBackoff`, 
`MailboxService.resetConnectBackoff` and `ChannelManager.resetConnectBackoff` 
are removed. A refresh also clears the backoff.
     - `MailboxService.shutdown()` also shuts down its client channels.
   
   ## Tests
   
   - `MailboxMisdeliveryTest` and `DispatchMisrouteTest` reproduce the incident 
with real gRPC servers. A test name resolver plays DNS, and a TCP relay plays 
the IP address that moves from A to B.
     - Without the check (older senders, or check off): the block lands on B as 
an orphan and A gets nothing. B runs A's stage plan.
     - With the check: B rejects, A gets the error at once, the meters count it 
once, and the next stream or plan reaches A. Cancel, explain and streaming 
submit are rejected too.
   - Unit tests: `ReceiverIdentity` (pinned hash, a negative hash and a 
non-ASCII id through the header), plan serialization, mailbox routing, mailbox 
assignment for every exchange kind in both planners, the refresh debounce, the 
timeout message, the orphan meter, the server refresh handler and the broker 
routing callback.
   - `QueryRunnerTest` and `ResourceBasedQueriesTest` now run with the check 
on. `MultiStageEngineIntegrationTest` checks that a real cluster rejects a 
wrong id and accepts its own. With wrong ids in the test servers, these tests 
fail, so the check is live.
   - The full `pinot-query-runtime`, `pinot-query-planner`, `pinot-broker` and 
`pinot-server` suites pass.
   
   ## Not in this PR
   
   - **Single-stage queries.** The broker resolves the hostname at each 
reconnect, so the risk is lower. A wrong server reports missing segments 
(partial results, not a hang). A fix needs the target instance in the thrift 
`InstanceRequest`.
   - **Time series.** `TimeSeriesQueryDispatcher` has its own channels with the 
same exposure.
   - **Proactive refresh for brokers and remote clusters.** Channels to a 
broker's mailbox, and to servers of other clusters, are not refreshed on 
restart. The receiver check still applies to them.
   


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