raghavyadav01 commented on code in PR #20021:
URL: https://github.com/apache/pinot/pull/20021#discussion_r4448541859
##########
pinot-core/src/main/java/org/apache/pinot/core/transport/ServerChannels.java:
##########
@@ -146,10 +174,67 @@ public Channel sendRequest(String rawTableName,
AsyncQueryResponse asyncQueryRes
ServerRoutingInstance serverRoutingInstance, InstanceRequest
instanceRequest, long timeoutMs)
throws Exception {
byte[] requestBytes =
THREAD_LOCAL_T_SERIALIZER.get().serialize(instanceRequest);
- ServerChannel serverChannel =
_serverToChannelMap.computeIfAbsent(serverRoutingInstance, ServerChannel::new);
- // This server is now query-carrying, whoever opened the channel. See
[#hasChannel].
- serverChannel._openedByQuery = true;
- return serverChannel.sendRequest(rawTableName, asyncQueryResponse,
serverRoutingInstance, requestBytes, timeoutMs);
+ // One deadline for the whole call: a sender that races an eviction
retries on a fresh entry, but the lock waits
+ // and connects of all its attempts together stay within the caller's
timeout.
+ long deadlineMs = System.currentTimeMillis() + timeoutMs;
+ for (int attempt = 1; attempt <= MAX_SEND_ATTEMPTS; attempt++) {
+ ServerChannel serverChannel =
_serverToChannelMap.computeIfAbsent(serverRoutingInstance, ServerChannel::new);
+ // This server is now query-carrying, whoever opened the channel. See
[#hasChannel].
+ serverChannel._openedByQuery = true;
+ Channel channel = serverChannel.sendRequest(rawTableName,
asyncQueryResponse, serverRoutingInstance,
+ requestBytes, Math.max(0L, deadlineMs - System.currentTimeMillis()));
+ if (channel != null) {
+ return channel;
+ }
+ // The entry was evicted between the lookup and the send (see
[#evictChannel]); it is already out of the map, so
+ // the next lookup creates a fresh one.
+ LOGGER.info("Channel to server: {} was evicted while sending a request,
retrying on a fresh channel (attempt {})",
+ serverRoutingInstance, attempt);
+ }
+ throw new IllegalStateException(
+ "Channel to server: " + serverRoutingInstance + " kept being evicted
while sending a request");
+ }
+
+ /// Closes and forgets the cached channel to the given routing instance, if
there is one. Returns `true` if that
+ /// closed a live connection. An entry whose channel had already closed (the
server sent a FIN as it shut down) or
+ /// never connected is dropped all the same but not counted, so that
+ /// [BrokerMeter#NETTY_CONNECTION_CHANNEL_EVICTIONS] reads as connections
that were still open, possibly half-open,
+ /// when the broker gave up on them, and a graceful rolling restart does not
bump it for every server.
+ ///
+ /// Called when the routing manager enables a server, which is what a
restart looks like through Helix, and when it
+ /// sees a server removed from the cluster. The cached channel then belongs
to a previous process: if that process
+ /// went away without a FIN (node flap, OOM kill, a network path that
stopped forwarding), the socket is half-open,
+ /// `isActive()` keeps reporting `true` until the kernel gives up on
retransmits, and every query routed to the
+ /// readmitted server would be written into it and lost. Evicting here makes
the next query reconnect, to the new
+ /// process at whatever address its hostname now resolves to.
+ ///
+ /// Queries in flight on the evicted channel fail fast through
`channelInactive` rather than waiting out their
+ /// timeout: the process they were sent to is gone. The entry is flagged
before it is closed so that a sender that
+ /// already holds it does not reconnect it after it has left the map, which
would leak a channel nobody routes to
+ /// (see [ServerChannel#sendRequest]).
+ public boolean evictChannel(ServerRoutingInstance serverRoutingInstance) {
+ ServerChannel serverChannel =
_serverToChannelMap.remove(serverRoutingInstance);
+ if (serverChannel == null) {
+ return false;
+ }
+ // Read before the eviction closes it; a snapshot is enough, as it only
decides what is counted
+ Channel channel = serverChannel._channel;
+ boolean wasConnected = channel != null && channel.isActive();
+ serverChannel.evict();
+ if (!wasConnected) {
+ LOGGER.debug("Dropped cached entry without a live channel to server:
{}", serverRoutingInstance);
+ return false;
+ }
+ LOGGER.info("Evicted live cached channel to server: {}",
serverRoutingInstance);
+
_brokerMetrics.addMeteredGlobalValue(BrokerMeter.NETTY_CONNECTION_CHANNEL_EVICTIONS,
1);
+ return true;
+ }
+
+ /// Whether [NettyConfig#TCP_USER_TIMEOUT_MS] is set on the channels: it is
a Linux socket option that Netty only
+ /// exposes on the Epoll transport.
+ @VisibleForTesting
+ boolean isTcpUserTimeoutApplied() {
+ return _tcpUserTimeoutMs > 0 && _channelClass == EpollSocketChannel.class;
Review Comment:
Native transports are off by default, so on a stock broker this never
applies. That leaves the write timeout, which only fires once the kernel send
buffer is full. With small requests at low QPS, a half-open socket keeps taking
writes while the kernel retransmits, and queries keep timing out against that
server for minutes, same as before. Could we add a check that closes a channel
when it has pinned requests outstanding and has read nothing for a while, or at
least say in the release note that TCP_USER_TIMEOUT is effectively off by
default?
##########
pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java:
##########
@@ -461,19 +501,22 @@ private void processInstanceConfigChangeInternal() {
// hit the identity fast path
instanceId = serverInstance.getInstanceId();
enabledServers.add(instanceId);
- if (_enabledServerInstanceMap.put(instanceId, serverInstance) ==
null) {
+ ServerInstance previousInstance =
_enabledServerInstanceMap.put(instanceId, serverInstance);
+ if (previousInstance == null) {
newEnabledServers.add(instanceId);
// NOTE: Remove new enabled server from excluded servers because
the server is likely being restarted
if (_excludedServers.remove(instanceId)) {
LOGGER.info("Got excluded server: {} re-enabled, including it
into the routing", instanceId);
}
- // The server likely restarted, possibly at a new address. The
callback refreshes the GRPC channel to it:
- // it clears any reconnection backoff, without which MSE queries
to this server may fail fast until the
- // next backoff retry succeeds, and it makes the channel resolve
the hostname again.
- if (_serverEnabledCallback != null) {
- _serverEnabledCallback.accept(serverInstance);
- }
+ notifyIfAddressChanged(_disabledServers.remove(instanceId),
serverInstance);
+ // The server likely restarted, possibly at a new address. The
callbacks drop what may be tied to its
+ // previous process: the GRPC channel's reconnection backoff,
without which MSE queries to this server may
+ // fail fast until the next backoff retry succeeds, and its stale
resolved address; cached Netty channels
+ // that may be half-open.
+ notifyServerEnabled(serverInstance);
Review Comment:
This fires on any disabled-to-enabled transition, not only after a restart.
If someone toggles QUERIES_DISABLED or HELIX_ENABLED on a live server,
re-enabling it closes a healthy channel and fails whatever is still in flight
on it, long-timeout queries included. Could eviction skip channels that have
received data since the server was disabled?
##########
pinot-core/src/main/java/org/apache/pinot/core/transport/AsyncQueryResponse.java:
##########
@@ -237,8 +263,27 @@ void cancelQuery(ServerRoutingInstance
serverRoutingInstance, Exception exceptio
/// Returns `true` if a server-down event should be acted on: the server was
queried by this response and has not
/// responded yet
private boolean shouldActOnServerDown(ServerRoutingInstance
serverRoutingInstance) {
+ return shouldActOnServerDown(serverRoutingInstance, null, true);
+ }
+
+ /// As above, and when `channel` is given the request must have been written
to that channel. A request whose
+ /// channel is not known yet (it is about to be written, possibly to a
channel that is still being connected) is
+ /// still taken down by a genuine failure of the server, as before, but
never by a close the broker itself made:
+ /// that close only concerns the queries demonstrably written to the closed
channel.
+ private boolean shouldActOnServerDown(ServerRoutingInstance
serverRoutingInstance, @Nullable Channel channel,
+ boolean reportServer) {
ServerResponse serverResponse = _responseMap.get(serverRoutingInstance);
- return serverResponse != null && serverResponse.getDataTable() == null;
+ if (serverResponse == null || serverResponse.getDataTable() != null) {
+ return false;
+ }
+ if (channel == null) {
+ return true;
+ }
+ Channel requestChannel = serverResponse.getChannel();
+ if (requestChannel == null) {
+ return reportServer;
Review Comment:
When the server closes the retired channel late, a sender waiting on the
channel lock for the replacement isn't pinned yet, so it still gets failed
here. That seems to undercut the point of scoping down events to the request
channel. When a channel is given, could we act only on requests pinned to that
channel and let unpinned ones fail on their own write or connect?
##########
pinot-common/src/main/java/org/apache/pinot/common/metrics/BrokerMeter.java:
##########
@@ -232,6 +232,20 @@ public class BrokerMeter implements AbstractMetrics.Meter {
"NETTY_CONNECTION_CHANNEL_ACTIVE", "nettyConnection", false);
public static final BrokerMeter NETTY_CONNECTION_CHANNEL_INACTIVE = create(
"NETTY_CONNECTION_CHANNEL_INACTIVE", "nettyConnection", false);
+ // Requests rejected because the channel's outbound queue was above its high
watermark, i.e. the send window to the
+ // server is closed (see NettyConfig.WRITE_BUFFER_HIGH_WATER_MARK_BYTES).
Each one is a query failed fast instead of
+ // being queued in direct memory behind a stalled socket.
+ public static final BrokerMeter NETTY_CONNECTION_UNWRITABLE_REJECTIONS =
create(
Review Comment:
These three new meters are global only, so when write timeouts or rejections
spike there's no way to tell from metrics which server is at fault.
NETTY_CONNECTION_CHANNEL_INACTIVE already tags the server. Could these use
addMeteredValue with the server's short name the same way?
##########
pinot-core/src/main/java/org/apache/pinot/core/transport/ServerChannels.java:
##########
@@ -365,11 +496,31 @@ private void awaitTlsHandshake(Channel channel, long
timeoutMs)
}
}
- void sendRequestWithoutLocking(String rawTableName, AsyncQueryResponse
asyncQueryResponse,
+ /// Returns the channel the request was written to.
+ Channel sendRequestWithoutLocking(String rawTableName, AsyncQueryResponse
asyncQueryResponse,
ServerRoutingInstance serverRoutingInstance, byte[] requestBytes) {
- long startTimeMs = System.currentTimeMillis();
- // The listener runs later, by when a reconnect may have replaced
_channel: on failure it closes this connection.
+ // Pin the channel: the completion listener must act on the channel it
wrote to, not on whatever channel this
+ // entry holds by the time the write completes, or a late failure would
close a healthy reconnected channel.
Channel channel = _channel;
+ if (!channel.isWritable()) {
Review Comment:
One large request near 16 MiB makes the channel unwritable for everyone
until it drains, so concurrent queries to a healthy server get rejected here.
Without skipUnavailableServers they fail with the server recorded as the failed
one, so the failure detector can briefly exclude a server that is fine. Could
this rejection skip blaming the server, or only reject when the channel has
stayed unwritable past a short grace period?
--
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]