This is an automated email from the ASF dual-hosted git repository.
yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new c5ac29be0e6 Refresh MSE gRPC channels when a server restarts (#19875)
c5ac29be0e6 is described below
commit c5ac29be0e6a3d5c5fae5c0ed79ece50f87847d9
Author: Yash Mayya <[email protected]>
AuthorDate: Sat Oct 24 15:24:56 2026 -0700
Refresh MSE gRPC channels when a server restarts (#19875)
---
.../broker/broker/helix/BaseBrokerStarter.java | 4 +-
.../routing/manager/BaseBrokerRoutingManager.java | 23 +--
.../routing/manager/BrokerRoutingManagerTest.java | 33 +++--
.../apache/pinot/query/mailbox/MailboxService.java | 18 ++-
.../query/mailbox/channel/ChannelManager.java | 73 ++++++++--
.../query/service/dispatch/DispatchClient.java | 21 +++
.../query/service/dispatch/QueryDispatcher.java | 36 +++--
.../query/mailbox/channel/ChannelManagerTest.java | 111 +++++++--------
.../service/dispatch/QueryDispatcherTest.java | 83 +++++++++++
.../server/starter/helix/BaseServerStarter.java | 10 +-
...ava => ServerMailboxChannelRefreshHandler.java} | 107 +++++++++++---
...=> ServerMailboxChannelRefreshHandlerTest.java} | 155 +++++++++++++++++----
12 files changed, 516 insertions(+), 158 deletions(-)
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
index 878d08f2db8..a9f4cf4a721 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
@@ -585,8 +585,8 @@ public abstract class BaseBrokerStarter implements
ServiceStartable {
_threadAccountant, multiClusterRoutingContext, workerManager,
multiClusterWorkerManager,
_serverRoutingStatsManager);
MultiStageBrokerRequestHandler finalHandler =
multiStageBrokerRequestHandler;
- _routingManager.setServerReenableCallback(
- serverInstance ->
finalHandler.getQueryDispatcher().resetClientConnectionBackoff(serverInstance));
+ _routingManager.setServerEnabledCallback(
+ serverInstance ->
finalHandler.getQueryDispatcher().onServerEnabled(serverInstance));
// Lets an operator turn the proto segment list encoding on and off
through cluster config, without a restart.
_clusterConfigChangeHandler.registerClusterConfigChangeListener(
multiStageBrokerRequestHandler.getQueryDispatcher());
diff --git
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
index 6503606f600..85f699db3ce 100644
---
a/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
+++
b/pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/BaseBrokerRoutingManager.java
@@ -145,7 +145,7 @@ public abstract class BaseBrokerRoutingManager
private final long _newSegmentExpirationMs;
private final ExecutorService _executorService;
@Nullable
- private Consumer<ServerInstance> _serverReenableCallback;
+ private Consumer<ServerInstance> _serverEnabledCallback;
// Providers of extra per-table segment ZK metadata fetch listeners,
registered alongside the built-in segment
// pruners on every table's routing entry. Each provider is invoked once per
table (with the table name with type)
@@ -235,10 +235,11 @@ public abstract class BaseBrokerRoutingManager
return _partitionPruningPreparationThreshold;
}
- /// Sets a callback to be invoked when a server is re-enabled after being
excluded.
- /// This is useful for resetting gRPC channel state to avoid exponential
backoff delays.
- public void setServerReenableCallback(Consumer<ServerInstance> callback) {
- _serverReenableCallback = callback;
+ /// Sets a callback to be invoked when a server becomes routable: when it
starts, and when it is enabled again after
+ /// it was disabled (for example while it restarted) or excluded. This is
useful for resetting gRPC channel state to
+ /// avoid exponential backoff delays and connections to a stale address.
+ public void setServerEnabledCallback(Consumer<ServerInstance> callback) {
+ _serverEnabledCallback = callback;
}
/// Registers a provider of an extra [SegmentZkMetadataFetchListener] that
is attached to every table's routing
@@ -466,12 +467,12 @@ public abstract class BaseBrokerRoutingManager
// 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);
- // We clear any GRPC channel reconnection backoff in this
callback when a server is re-enabled. Otherwise,
- // MSE queries to this server may fail fast until the next
backoff retry succeeds.
- if (_serverReenableCallback != null) {
- LOGGER.info("Calling server re-enable callback for server:
{}", instanceId);
- _serverReenableCallback.accept(serverInstance);
- }
+ }
+ // 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);
}
}
}
diff --git
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
index 4bbd9d88535..e56065a649e 100644
---
a/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
+++
b/pinot-broker/src/test/java/org/apache/pinot/broker/routing/manager/BrokerRoutingManagerTest.java
@@ -124,7 +124,7 @@ public class BrokerRoutingManagerTest {
private PropertyKey _instanceConfigsKey;
@Mock
- private Consumer<ServerInstance> _serverReenableCallback;
+ private Consumer<ServerInstance> _serverEnabledCallback;
private BrokerRoutingManager _routingManager;
@@ -284,9 +284,9 @@ public class BrokerRoutingManagerTest {
}
@Test
- public void testServerReenableCallbackInvokedWhenExcludedServerReenabled() {
+ public void testServerEnabledCallbackInvokedWhenExcludedServerReenabled() {
// Set up callback
- _routingManager.setServerReenableCallback(_serverReenableCallback);
+ _routingManager.setServerEnabledCallback(_serverEnabledCallback);
// First, enable the server by processing instance config change
List<ZNRecord> instanceConfigs =
List.of(createEnabledServerZNRecord(SERVER_INSTANCE_ID));
@@ -312,29 +312,40 @@ public class BrokerRoutingManagerTest {
anyInt(), anyInt())).thenReturn(instanceConfigs);
_routingManager.processClusterChange(ChangeType.INSTANCE_CONFIG);
- // Verify callback was invoked with correct ServerInstance
+ // Verify callback was invoked with correct ServerInstance: when the
server was first enabled, and when it was
+ // enabled again
ArgumentCaptor<ServerInstance> captor =
ArgumentCaptor.forClass(ServerInstance.class);
- verify(_serverReenableCallback).accept(captor.capture());
+ verify(_serverEnabledCallback, times(2)).accept(captor.capture());
ServerInstance capturedInstance = captor.getValue();
assertEquals(capturedInstance.getHostname(), SERVER_HOST);
assertEquals(capturedInstance.getPort(), SERVER_PORT);
}
+ /// A server that restarts without having been excluded is disabled while it
shuts down and enabled again once it
+ /// is ready. It may be back at a new address, so the callback must run for
it too, to refresh the channel to it.
@Test
- public void testServerReenableCallbackNotInvokedForNewServer() {
- // Set up callback
- _routingManager.setServerReenableCallback(_serverReenableCallback);
+ public void testServerEnabledCallbackInvokedWhenRestartedServerReenabled() {
+ _routingManager.setServerEnabledCallback(_serverEnabledCallback);
- // Enable a new server (never excluded)
List<ZNRecord> instanceConfigs =
List.of(createEnabledServerZNRecord(SERVER_INSTANCE_ID));
when(_zkDataAccessor.getChildren(eq(INSTANCE_CONFIGS_PATH), any(),
eq(AccessOption.PERSISTENT),
anyInt(), anyInt())).thenReturn(instanceConfigs);
+ _routingManager.processClusterChange(ChangeType.INSTANCE_CONFIG);
+ verify(_serverEnabledCallback, times(1)).accept(any());
+ // A change that leaves the server enabled does not call it again
_routingManager.processClusterChange(ChangeType.INSTANCE_CONFIG);
+ verify(_serverEnabledCallback, times(1)).accept(any());
- // Verify callback was NOT invoked (server was never excluded)
- verify(_serverReenableCallback, never()).accept(any());
+ // Disabled while it restarts, then enabled again
+ when(_zkDataAccessor.getChildren(eq(INSTANCE_CONFIGS_PATH), any(),
eq(AccessOption.PERSISTENT),
+ anyInt(), anyInt())).thenReturn(List.of());
+ _routingManager.processClusterChange(ChangeType.INSTANCE_CONFIG);
+ when(_zkDataAccessor.getChildren(eq(INSTANCE_CONFIGS_PATH), any(),
eq(AccessOption.PERSISTENT),
+ anyInt(), anyInt())).thenReturn(instanceConfigs);
+ _routingManager.processClusterChange(ChangeType.INSTANCE_CONFIG);
+ verify(_serverEnabledCallback, times(2)).accept(any());
}
@Test
diff --git
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/MailboxService.java
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/MailboxService.java
index 0a130b6692f..8309e84d42c 100644
---
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/MailboxService.java
+++
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/MailboxService.java
@@ -193,6 +193,7 @@ public class MailboxService {
public void shutdown() {
LOGGER.info("Shutting down GrpcMailboxServer");
_grpcMailboxServer.shutdown();
+ _channelManager.shutdown();
}
public String getHostname() {
@@ -232,11 +233,18 @@ public class MailboxService {
}
}
- /// Resets the GRPC connection backoff for the channel to the given server.
- /// @see ChannelManager#resetConnectBackoff(String, int)
- /// @return true if the channel was in TRANSIENT_FAILURE and backoff was
reset
- public boolean resetConnectBackoff(String hostname, int port) {
- return _channelManager.resetConnectBackoff(hostname, port);
+ /// Makes the channel to the given server resolve its hostname again before
the next send.
+ /// @see ChannelManager#refreshChannel(String, int)
+ /// @return false if there is no channel to the server, or it was just
refreshed
+ public boolean refreshChannel(String hostname, int port) {
+ return _channelManager.refreshChannel(hostname, port);
+ }
+
+ /// Shuts down the channel to an address that is no longer used.
+ /// @see ChannelManager#retireChannel(String, int)
+ /// @return false if there is no channel to the address
+ public boolean retireChannel(String hostname, int port) {
+ return _channelManager.retireChannel(hostname, port);
}
/// Current value of [BrokerGauge#MAILBOX_CLIENT_USED_DIRECT_MEMORY] /
diff --git
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/channel/ChannelManager.java
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/channel/ChannelManager.java
index b9b06816935..c238422c102 100644
---
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/channel/ChannelManager.java
+++
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/mailbox/channel/ChannelManager.java
@@ -29,6 +29,7 @@ import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.pinot.query.grpc.GrpcKeepAliveConfig;
@@ -43,8 +44,13 @@ import org.slf4j.LoggerFactory;
public class ChannelManager {
private static final Logger LOGGER =
LoggerFactory.getLogger(ChannelManager.class);
+ /// A channel is refreshed at most once in this interval, see
[#refreshChannel].
+ private static final long MIN_REFRESH_INTERVAL_NS =
TimeUnit.SECONDS.toNanos(1);
+
/// Map from (hostname, port) to the ManagedChannel with all known channels
private final ConcurrentHashMap<Pair<String, Integer>, ManagedChannel>
_channelMap = new ConcurrentHashMap<>();
+ /// When each channel was last refreshed, from [System#nanoTime()], by
(hostname, port), see [#refreshChannel].
+ private final ConcurrentHashMap<Pair<String, Integer>, AtomicLong>
_lastRefreshTimeNs = new ConcurrentHashMap<>();
/// The idle timeout for the channel, which cannot be disabled in gRPC.
///
/// In general we want to prevent the channel from going idle, so that we
don't have to re-establish the connection
@@ -140,7 +146,7 @@ public class ChannelManager {
return;
}
ConnectivityState next = channel.getState(false);
- if (state == ConnectivityState.READY && next != ConnectivityState.READY)
{
+ if (state == ConnectivityState.READY && next != ConnectivityState.READY
&& !isRecentlyRefreshed(hostname, port)) {
LOGGER.warn("Mailbox channel to {}:{} left READY for {}; sends to that
peer will fail until it "
+ "reconnects. If keep-alive reported it, the peer stopped
answering about one keep-alive "
+ "interval ago.", hostname, port, next);
@@ -150,18 +156,63 @@ public class ChannelManager {
return channel;
}
- /// Resets the connection backoff for the channel to the given server if the
channel is in
- /// TRANSIENT_FAILURE state. Returns true if a reset was performed, false
otherwise.
+ /// Makes the channel to the given address resolve the hostname again and
open a new connection before its next
+ /// stream, dropping the connection it has, and any reconnect backoff.
Streams in flight finish on the old
+ /// connection. Returns false if there is no channel to the address, or if
it was refreshed less than a second ago.
+ ///
+ /// A channel resolves the hostname only when it starts, and again after a
connection fails or is lost. When the
+ /// server at that address goes away, gRPC resolves once, right then, and
keeps that answer until the next failure.
+ /// If DNS still had the old IP address at that moment (a terminating pod
stays in DNS for a while), the channel
+ /// later reconnects to the old address, and when another server took that
address over and listens on the same
+ /// port, the connection succeeds and is never questioned again. Call this
when the address can be stale, for example
+ /// when the server at the address has restarted.
///
- /// @return true if the channel was in TRANSIENT_FAILURE and backoff was
reset
- public boolean resetConnectBackoff(String hostname, int port) {
- ManagedChannel channel = _channelMap.get(Pair.of(hostname, port));
- if (channel != null && channel.getState(false) ==
ConnectivityState.TRANSIENT_FAILURE) {
- LOGGER.info("Resetting mailbox channel backoff for server: {}:{}",
hostname, port);
- channel.resetConnectBackoff();
- return true;
+ /// Moving the channel to IDLE, rather than replacing it, keeps the channel
object that concurrent senders may
+ /// already hold valid. The refreshes are limited to one a second, because
each refresh drops the connection that the
+ /// previous one opened.
+ public boolean refreshChannel(String hostname, int port) {
+ Pair<String, Integer> key = Pair.of(hostname, port);
+ ManagedChannel channel = _channelMap.get(key);
+ if (channel == null) {
+ return false;
+ }
+ long nowNs = System.nanoTime();
+ AtomicLong lastRefreshTimeNs =
+ _lastRefreshTimeNs.computeIfAbsent(key, k -> new AtomicLong(nowNs -
MIN_REFRESH_INTERVAL_NS));
+ long lastNs = lastRefreshTimeNs.get();
+ if (nowNs - lastNs < MIN_REFRESH_INTERVAL_NS ||
!lastRefreshTimeNs.compareAndSet(lastNs, nowNs)) {
+ return false;
+ }
+ channel.enterIdle();
+ return true;
+ }
+
+ private boolean isRecentlyRefreshed(String hostname, int port) {
+ AtomicLong lastRefreshTimeNs = _lastRefreshTimeNs.get(Pair.of(hostname,
port));
+ return lastRefreshTimeNs != null && System.nanoTime() -
lastRefreshTimeNs.get() < MIN_REFRESH_INTERVAL_NS;
+ }
+
+ /// Removes the channel to the given address and shuts it down, letting
streams in flight finish. Use it for an
+ /// address that is no longer used, such as the old mailbox port of a server
that restarted on another port, so
+ /// that channels do not pile up. Returns false if there is no channel to
the address.
+ public boolean retireChannel(String hostname, int port) {
+ Pair<String, Integer> key = Pair.of(hostname, port);
+ ManagedChannel channel = _channelMap.remove(key);
+ if (channel == null) {
+ return false;
+ }
+ _lastRefreshTimeNs.remove(key);
+ channel.shutdown();
+ return true;
+ }
+
+ /// Shuts down all channels, letting streams in flight finish.
+ public void shutdown() {
+ for (ManagedChannel channel : _channelMap.values()) {
+ channel.shutdown();
}
- return false;
+ _channelMap.clear();
+ _lastRefreshTimeNs.clear();
}
private NettyChannelBuilder decorate(NettyChannelBuilder builder) {
diff --git
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/DispatchClient.java
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/DispatchClient.java
index 05c5f0905b0..2c1ce9e6c5a 100644
---
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/DispatchClient.java
+++
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/DispatchClient.java
@@ -26,6 +26,8 @@ import io.grpc.netty.shaded.io.netty.channel.ChannelOption;
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.grpc.stub.StreamObserver;
import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.annotation.Nullable;
@@ -50,8 +52,13 @@ class DispatchClient {
/// Using a static allocator allows for better memory pooling across all
DispatchClient instances.
private static final PooledByteBufAllocator BUF_ALLOCATOR = new
PooledByteBufAllocator(true);
+ /// The channel is refreshed at most once in this interval, see
[#refreshChannel].
+ private static final long MIN_REFRESH_INTERVAL_NS =
TimeUnit.SECONDS.toNanos(1);
+
private final ManagedChannel _channel;
private final PinotQueryWorkerGrpc.PinotQueryWorkerStub _dispatchStub;
+ /// When the channel was last refreshed, from [System#nanoTime()], see
[#refreshChannel].
+ private final AtomicLong _lastRefreshTimeNs = new
AtomicLong(System.nanoTime() - MIN_REFRESH_INTERVAL_NS);
public DispatchClient(String host, int port, @Nullable TlsConfig tlsConfig) {
this(host, port, tlsConfig, null, GrpcKeepAliveConfig.DISABLED);
@@ -99,6 +106,20 @@ class DispatchClient {
_dispatchStub.withDeadline(deadline).submit(request, new
LastValueDispatchObserver<>(virtualServer, callback));
}
+ /// Makes the channel resolve the server's hostname again and open a new
connection, without reconnect backoff, for
+ /// the next request. Requests in flight finish on the old connection. Does
nothing, and returns false, if the
+ /// channel was refreshed less than a second ago: each refresh drops the
connection that the previous one opened.
+ public boolean refreshChannel() {
+ long nowNs = System.nanoTime();
+ long lastRefreshTimeNs = _lastRefreshTimeNs.get();
+ if (nowNs - lastRefreshTimeNs < MIN_REFRESH_INTERVAL_NS
+ || !_lastRefreshTimeNs.compareAndSet(lastRefreshTimeNs, nowNs)) {
+ return false;
+ }
+ _channel.enterIdle();
+ return true;
+ }
+
/// Opens a `SubmitWithStream` bidi RPC for one server, sends the initial
`submit`, and registers the
/// resulting [StreamingDispatchObserver] with `session` for cancel fan-out
and `OpChainComplete`
/// accumulation.
diff --git
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
index b3a1e605da6..d2897d6ef0f 100644
---
a/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
+++
b/pinot-query-runtime/src/main/java/org/apache/pinot/query/service/dispatch/QueryDispatcher.java
@@ -124,6 +124,9 @@ public class QueryDispatcher implements
PinotClusterConfigChangeListener {
private final MailboxService _mailboxService;
private final ExecutorService _executorService;
private final Map<String, DispatchClient> _dispatchClientMap = new
ConcurrentHashMap<>();
+ /// Last known dispatch address of each server, by instance id, kept by
[#onServerEnabled] to retire the dispatch
+ /// client of an address a server has left.
+ private final Map<String, String> _dispatchAddressByInstanceId = new
ConcurrentHashMap<>();
@Nullable
private final TlsConfig _tlsConfig;
@Nullable
@@ -896,16 +899,31 @@ public class QueryDispatcher implements
PinotClusterConfigChangeListener {
_dispatchMaxInboundMessageSizeBytes));
}
- /// Reset the connection backoff for a server. When the GRPC channel enters
a TRANSIENT_FAILURE state from
- /// connection failures, it will fast fail requests and reconnect with
exponential backoff. This method
- /// resets the backoff so servers that have recovered can be reconnected to
immediately.
- public void resetClientConnectionBackoff(ServerInstance serverInstance) {
- String hostname = serverInstance.getHostname();
- int port = serverInstance.getQueryServicePort();
- DispatchClient dispatchClient =
_dispatchClientMap.get(toHostnamePortKey(hostname, port));
+ /// Called when a server becomes routable, which it does when it starts and
when it is enabled again after it was
+ /// disabled or excluded. The server may have restarted at a new address, so:
+ ///
+ /// - the channel to its address resolves the hostname again before the next
request, and connects without
+ /// reconnect backoff. A channel resolves only when a connection fails or
is lost, and the last time was likely
+ /// when the server went away, while DNS could still have the old IP
address. Reconnecting to that address could
+ /// reach another server that took it over.
+ /// - the channel to its previous address, if the server moved to another
port, is shut down, since nothing uses it
+ /// again.
+ public void onServerEnabled(ServerInstance serverInstance) {
+ String hostnamePortKey = toHostnamePortKey(serverInstance.getHostname(),
serverInstance.getQueryServicePort());
+ String previousKey =
_dispatchAddressByInstanceId.put(serverInstance.getInstanceId(),
hostnamePortKey);
+ if (previousKey != null && !previousKey.equals(hostnamePortKey)
+ && !_dispatchAddressByInstanceId.containsValue(previousKey)) {
+ DispatchClient previousClient = _dispatchClientMap.remove(previousKey);
+ if (previousClient != null) {
+ LOGGER.info("Server: {} moved from {} to {}, shutting down the
dispatch channel to the old address",
+ serverInstance.getInstanceId(), previousKey, hostnamePortKey);
+ previousClient.getChannel().shutdown();
+ }
+ }
+ DispatchClient dispatchClient = _dispatchClientMap.get(hostnamePortKey);
if (dispatchClient != null) {
- LOGGER.info("Resetting connection backoff for server: {}",
serverInstance.getInstanceId());
- dispatchClient.getChannel().resetConnectBackoff();
+ LOGGER.info("Server: {} is enabled, refreshing the dispatch channel to
it", serverInstance.getInstanceId());
+ dispatchClient.refreshChannel();
}
}
diff --git
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/channel/ChannelManagerTest.java
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/channel/ChannelManagerTest.java
index 0eab4e2bde2..1f0bb9dcd6e 100644
---
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/channel/ChannelManagerTest.java
+++
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/mailbox/channel/ChannelManagerTest.java
@@ -20,76 +20,20 @@ package org.apache.pinot.query.mailbox.channel;
import io.grpc.ConnectivityState;
import io.grpc.ManagedChannel;
-import java.lang.reflect.Field;
import java.time.Duration;
-import java.util.concurrent.ConcurrentHashMap;
-import org.apache.commons.lang3.tuple.Pair;
import org.apache.pinot.query.grpc.GrpcKeepAliveConfig;
import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.util.TestUtils;
import org.testng.annotations.Test;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
public class ChannelManagerTest {
- @Test
- public void testResetConnectBackoffNoOpForUnknownChannel() {
- ChannelManager channelManager = new ChannelManager(null, 4_000_000,
Duration.ofDays(365),
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_HIGH_WATER_MARK_BYTES,
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_LOW_WATER_MARK_BYTES,
- GrpcKeepAliveConfig.DISABLED);
- // Should return false and not throw when no channel exists for the given
host/port
- assertFalse(channelManager.resetConnectBackoff("unknown-host", 12345));
- }
-
- @Test
- public void testResetConnectBackoffNoOpWhenNotInTransientFailure() {
- ChannelManager channelManager = new ChannelManager(null, 4_000_000,
Duration.ofDays(365),
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_HIGH_WATER_MARK_BYTES,
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_LOW_WATER_MARK_BYTES,
- GrpcKeepAliveConfig.DISABLED);
- // Create a channel by calling getChannel
- ManagedChannel channel = channelManager.getChannel("localhost", 12345);
- try {
- // Reset backoff should return false since channel is not in
TRANSIENT_FAILURE
- assertFalse(channelManager.resetConnectBackoff("localhost", 12345));
- // Channel should still be the same cached instance
- ManagedChannel sameChannel = channelManager.getChannel("localhost",
12345);
- assertSame(sameChannel, channel);
- } finally {
- channel.shutdownNow();
- }
- }
-
- @Test
- @SuppressWarnings("unchecked")
- public void testResetConnectBackoffResetsWhenInTransientFailure()
- throws Exception {
- ChannelManager channelManager = new ChannelManager(null, 4_000_000,
Duration.ofDays(365),
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_HIGH_WATER_MARK_BYTES,
-
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_LOW_WATER_MARK_BYTES,
- GrpcKeepAliveConfig.DISABLED);
-
- ManagedChannel mockChannel = mock(ManagedChannel.class);
-
when(mockChannel.getState(false)).thenReturn(ConnectivityState.TRANSIENT_FAILURE);
-
- // Inject the mock channel into _channelMap via reflection
- Field channelMapField =
ChannelManager.class.getDeclaredField("_channelMap");
- channelMapField.setAccessible(true);
- ConcurrentHashMap<Pair<String, Integer>, ManagedChannel> channelMap =
- (ConcurrentHashMap<Pair<String, Integer>, ManagedChannel>)
channelMapField.get(channelManager);
- channelMap.put(Pair.of("failing-host", 9999), mockChannel);
-
- assertTrue(channelManager.resetConnectBackoff("failing-host", 9999));
- verify(mockChannel).resetConnectBackoff();
- }
-
/// Pins the fail-fast gate added in commit 1d29438dc0 ("Fail fast on
invalid gRPC mailbox transport
/// configuration"): a non-positive `writeBufferLowWaterMarkBytes` must
throw at startup rather than
/// surfacing later as a Netty `WriteBufferWaterMark` constructor failure on
the first send.
@@ -135,6 +79,57 @@ public class ChannelManagerTest {
}
}
+ /// Refreshing keeps the channel object, which senders may hold, and only
drops what it resolved and connected to.
+ @Test
+ public void testRefreshChannel() {
+ ChannelManager channelManager = newChannelManager();
+ assertFalse(channelManager.refreshChannel("localhost", 12348));
+ ManagedChannel channel = channelManager.getChannel("localhost", 12348);
+ try {
+ // Nothing listens there: the connect attempt fails, but takes the
channel out of IDLE
+ channel.getState(true);
+ assertTrue(channelManager.refreshChannel("localhost", 12348));
+ // A stale connection fails many streams at once: only the first one
drops the connection
+ assertFalse(channelManager.refreshChannel("localhost", 12348));
+ TestUtils.waitForCondition(aVoid -> channel.getState(false) ==
ConnectivityState.IDLE, 10_000L,
+ "The channel did not go back to IDLE");
+ assertFalse(channel.isShutdown());
+ assertSame(channelManager.getChannel("localhost", 12348), channel);
+
+ // Once a second has passed, the channel can be refreshed again
+ channel.getState(true);
+ TestUtils.waitForCondition(aVoid ->
channelManager.refreshChannel("localhost", 12348), 10_000L,
+ "The channel was not refreshed again");
+ TestUtils.waitForCondition(aVoid -> channel.getState(false) ==
ConnectivityState.IDLE, 10_000L,
+ "The channel did not go back to IDLE");
+ } finally {
+ channelManager.shutdown();
+ }
+ }
+
+ @Test
+ public void testRetireChannelAndShutdown() {
+ ChannelManager channelManager = newChannelManager();
+ assertFalse(channelManager.retireChannel("localhost", 12349));
+ ManagedChannel retired = channelManager.getChannel("localhost", 12349);
+ assertTrue(channelManager.retireChannel("localhost", 12349));
+ assertTrue(retired.isShutdown());
+ ManagedChannel replacement = channelManager.getChannel("localhost", 12349);
+ assertNotSame(replacement, retired);
+
+ ManagedChannel other = channelManager.getChannel("localhost", 12350);
+ channelManager.shutdown();
+ assertTrue(replacement.isShutdown());
+ assertTrue(other.isShutdown());
+ }
+
+ private static ChannelManager newChannelManager() {
+ return new ChannelManager(null, 4_000_000, Duration.ofDays(365),
+
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_HIGH_WATER_MARK_BYTES,
+
CommonConstants.MultiStageQueryRunner.DEFAULT_GRPC_WRITE_BUFFER_LOW_WATER_MARK_BYTES,
+ GrpcKeepAliveConfig.DISABLED);
+ }
+
/// A disabled policy must leave the builder alone rather than passing `-1`
to gRPC, which throws.
@Test
public void testDisabledKeepAliveStillBuildsChannel() {
diff --git
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
index d0b4caae606..ee225479c1d 100644
---
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
+++
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/service/dispatch/QueryDispatcherTest.java
@@ -18,6 +18,8 @@
*/
package org.apache.pinot.query.service.dispatch;
+import io.grpc.ConnectivityState;
+import io.grpc.ManagedChannel;
import io.grpc.stub.StreamObserver;
import java.time.Duration;
import java.util.ArrayList;
@@ -29,9 +31,11 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
+import org.apache.helix.model.InstanceConfig;
import org.apache.pinot.common.failuredetector.FailureDetector;
import org.apache.pinot.common.metrics.BrokerMetrics;
import org.apache.pinot.common.proto.Worker;
+import org.apache.pinot.core.transport.ServerInstance;
import
org.apache.pinot.core.transport.server.routing.stats.ServerRoutingStatsManager;
import org.apache.pinot.query.QueryEnvironment;
import org.apache.pinot.query.QueryEnvironmentTestBase;
@@ -137,6 +141,85 @@ public class QueryDispatcherTest extends QueryTestSet {
}
}
+ /// When a server becomes routable, the dispatch channel to it resolves the
hostname again, and the channel to an
+ /// address it left is shut down.
+ @Test
+ public void testServerEnabledRefreshesAndRetiresChannels() {
+ QueryDispatcher dispatcher =
+ new QueryDispatcher(Mockito.mock(MailboxService.class),
Mockito.mock(FailureDetector.class), null, false,
+ Duration.ofSeconds(1));
+ try {
+ // Nothing listens there
+ int port = QueryTestUtils.getAvailablePort();
+ QueryServerInstance server = new
QueryServerInstance("Server_localhost_8098", "localhost", port, 8101);
+ DispatchClient client = dispatcher.getOrCreateDispatchClient(server);
+ ManagedChannel channel = client.getChannel();
+ // Connecting fails, but takes the channel out of IDLE
+ channel.getState(true);
+ dispatcher.onServerEnabled(serverInstance("Server_localhost_8098",
port));
+ Assert.assertSame(dispatcher.getOrCreateDispatchClient(server), client);
+ TestUtils.waitForCondition(aVoid -> channel.getState(false) ==
ConnectivityState.IDLE, 10_000L,
+ "The channel did not go back to IDLE");
+ Assert.assertFalse(channel.isShutdown());
+
+ // The server restarted on another port
+ dispatcher.onServerEnabled(serverInstance("Server_localhost_8098", port
+ 1));
+ Assert.assertTrue(channel.isShutdown());
+ Assert.assertNotSame(dispatcher.getOrCreateDispatchClient(server),
client);
+ } finally {
+ dispatcher.shutdown();
+ }
+ }
+
+ /// The dispatch channel to an address stays when the server that had the
address moves away, if another server has
+ /// taken the address over.
+ @Test
+ public void testServerEnabledKeepsChannelToAddressTakenOver() {
+ QueryDispatcher dispatcher =
+ new QueryDispatcher(Mockito.mock(MailboxService.class),
Mockito.mock(FailureDetector.class), null, false,
+ Duration.ofSeconds(1));
+ try {
+ int port = QueryTestUtils.getAvailablePort();
+ dispatcher.onServerEnabled(serverInstance("Server_localhost_8098",
port));
+ DispatchClient client = dispatcher.getOrCreateDispatchClient(
+ new QueryServerInstance("Server_localhost_8098", "localhost", port,
8101));
+
+ // Another server takes over the address, then the server that had it
comes back on another port
+ dispatcher.onServerEnabled(serverInstance("Server_localhost_8099",
port));
+ dispatcher.onServerEnabled(serverInstance("Server_localhost_8098", port
+ 1));
+
+ Assert.assertFalse(client.getChannel().isShutdown());
+ Assert.assertSame(dispatcher.getOrCreateDispatchClient(
+ new QueryServerInstance("Server_localhost_8099", "localhost", port,
8101)), client);
+ } finally {
+ dispatcher.shutdown();
+ }
+ }
+
+ /// Each refresh drops the connection that the previous one opened, so a
channel is refreshed at most once a second.
+ @Test
+ public void testDispatchChannelRefreshIsDebounced() {
+ DispatchClient client = new DispatchClient("localhost",
QueryTestUtils.getAvailablePort(), null);
+ try {
+ Assert.assertTrue(client.refreshChannel());
+ Assert.assertFalse(client.refreshChannel());
+ TestUtils.waitForCondition(aVoid -> client.refreshChannel(), 10_000L,
"The channel was not refreshed again");
+ } finally {
+ client.getChannel().shutdownNow();
+ }
+ }
+
+ private static ServerInstance serverInstance(String instanceId, int
queryServerPort) {
+ InstanceConfig instanceConfig = new InstanceConfig(instanceId);
+ instanceConfig.setHostName("localhost");
+ instanceConfig.setPort(instanceId.substring(instanceId.lastIndexOf('_') +
1));
+ instanceConfig.getRecord()
+
.setIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_SERVICE_PORT_KEY,
queryServerPort);
+ instanceConfig.getRecord()
+
.setIntField(CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY,
8101);
+ return new ServerInstance(instanceConfig);
+ }
+
@AfterClass
public void tearDown() {
_queryDispatcher.shutdown();
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BaseServerStarter.java
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BaseServerStarter.java
index 7d90df3ac06..248d1d96210 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BaseServerStarter.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/BaseServerStarter.java
@@ -853,19 +853,19 @@ public abstract class BaseServerStarter implements
ServiceStartable {
}
}
- // Register handler to reset GRPC mailbox channel backoff when servers
come online
+ // Register handler to refresh the GRPC mailbox channels to servers when
they come online
WorkerQueryServer workerQueryServer =
_serverInstance.getWorkerQueryServer();
if (workerQueryServer != null) {
MailboxService mailboxService =
workerQueryServer.getQueryRunner().getMailboxService();
- LOGGER.info("Registering ServerGrpcChannelBackoffResetHandler");
+ LOGGER.info("Registering ServerMailboxChannelRefreshHandler");
try {
_helixManager.addInstanceConfigChangeListener(
- new ServerGrpcChannelBackoffResetHandler(_helixAdmin,
_helixClusterName, _instanceId, mailboxService));
+ new ServerMailboxChannelRefreshHandler(_helixAdmin,
_helixClusterName, _instanceId, mailboxService));
} catch (Exception e) {
- LOGGER.error("Failed to register
ServerGrpcChannelBackoffResetHandler", e);
+ LOGGER.error("Failed to register ServerMailboxChannelRefreshHandler",
e);
}
} else {
- LOGGER.info("Multi-stage query engine not enabled, skipping
ServerGrpcChannelBackoffResetHandler registration");
+ LOGGER.info("Multi-stage query engine not enabled, skipping
ServerMailboxChannelRefreshHandler registration");
}
// Start restlet server for admin API endpoint
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandler.java
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
similarity index 53%
rename from
pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandler.java
rename to
pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
index 28f5f173f92..331d88e12d9 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandler.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
@@ -18,10 +18,13 @@
*/
package org.apache.pinot.server.starter.helix;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
+import org.apache.commons.lang3.tuple.Pair;
import org.apache.helix.HelixAdmin;
import org.apache.helix.NotificationContext;
import org.apache.helix.api.listeners.BatchMode;
@@ -36,8 +39,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/// Listens for instance config changes and resets GRPC mailbox channel
backoff when a server
-/// completes startup (IS_SHUTDOWN_IN_PROGRESS transitions from true to false).
+/// Listens for instance config changes and refreshes the GRPC mailbox channel
to a server when it completes startup
+/// (IS_SHUTDOWN_IN_PROGRESS transitions from true to false).
///
/// When a server restarts (gracefully or after a crash), the startup sequence
is:
///
@@ -46,13 +49,19 @@ import org.slf4j.LoggerFactory;
/// 3. Query/gRPC server starts listening
/// 4. IS_SHUTDOWN_IN_PROGRESS set to false (readiness signal)
///
-/// This handler detects the transition in step 4 and resets the GRPC channel
backoff for
-/// that specific server, but only if the channel is currently in
TRANSIENT_FAILURE state.
-/// Channels in other states (READY, IDLE, CONNECTING) are left untouched.
+/// This handler detects the transition in step 4 and refreshes the channel to
that server: the channel resolves the
+/// server's hostname again before the next send, and connects without waiting
for its reconnect backoff. Resolving
+/// again matters because the restarted server may have a new IP address. The
channel last resolved the hostname when
+/// the old connection was lost, while DNS could still return the old address,
and it would reconnect there. When
+/// another server took over that address and listens on the same port, the
connection succeeds and the data goes to
+/// the wrong server.
+///
+/// It also shuts down the channel to the old mailbox address of a server that
moved to another address (for example a
+/// new port after a restart) or left the cluster, since nothing will use it
again.
@BatchMode(enabled = false)
@PreFetch(enabled = false)
-public class ServerGrpcChannelBackoffResetHandler implements
InstanceConfigChangeListener {
- private static final Logger LOGGER =
LoggerFactory.getLogger(ServerGrpcChannelBackoffResetHandler.class);
+public class ServerMailboxChannelRefreshHandler implements
InstanceConfigChangeListener {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ServerMailboxChannelRefreshHandler.class);
private final HelixAdmin _helixAdmin;
private final String _clusterName;
@@ -60,10 +69,12 @@ public class ServerGrpcChannelBackoffResetHandler
implements InstanceConfigChang
private final MailboxService _mailboxService;
// Tracks servers that have IS_SHUTDOWN_IN_PROGRESS=true. When a server
transitions
- // from being in this set to having IS_SHUTDOWN_IN_PROGRESS=false, we reset
its channel backoff.
+ // from being in this set to having IS_SHUTDOWN_IN_PROGRESS=false, we
refresh its channel.
private final Set<String> _shuttingDownServers = new HashSet<>();
+ // Last known mailbox address of each server, to retire the channel of an
address a server has left
+ private final Map<String, Pair<String, Integer>> _mailboxAddresses = new
HashMap<>();
- public ServerGrpcChannelBackoffResetHandler(HelixAdmin helixAdmin, String
clusterName, String selfInstanceId,
+ public ServerMailboxChannelRefreshHandler(HelixAdmin helixAdmin, String
clusterName, String selfInstanceId,
MailboxService mailboxService) {
_helixAdmin = helixAdmin;
_clusterName = clusterName;
@@ -93,20 +104,43 @@ public class ServerGrpcChannelBackoffResetHandler
implements InstanceConfigChang
}
/// Full cluster scan: used on INIT (first callback) and when instances are
added/removed.
+ ///
+ /// It records the new address of every server before it retires any old
one. Otherwise, when one server left an
+ /// address and a server read later in the scan took it over, the scan would
shut down the channel the second server
+ /// uses.
private void handleFullScan() {
Set<String> currentShuttingDown = new HashSet<>();
+ Set<String> currentServers = new HashSet<>();
+ Map<String, Pair<String, Integer>> previousAddresses = new HashMap<>();
for (String instance : _helixAdmin.getInstancesInCluster(_clusterName)) {
if (!InstanceTypeUtils.isServer(instance) ||
instance.equals(_selfInstanceId)) {
continue;
}
+ // Before reading the config: a server whose config cannot be read now
has not left the cluster
+ currentServers.add(instance);
InstanceConfig config = getInstanceConfig(instance);
if (config == null) {
continue;
}
+ Pair<String, Integer> previousAddress = recordMailboxAddress(instance,
config);
+ if (previousAddress != null) {
+ previousAddresses.put(instance, previousAddress);
+ }
if (isShutdownInProgress(config)) {
currentShuttingDown.add(instance);
} else if (_shuttingDownServers.contains(instance)) {
- resetBackoffForServer(instance, config);
+ refreshChannelToServer(instance, config);
+ }
+ }
+ // Servers that left the cluster
+ for (String instance : new HashSet<>(_mailboxAddresses.keySet())) {
+ if (!currentServers.contains(instance)) {
+ previousAddresses.put(instance, _mailboxAddresses.remove(instance));
+ }
+ }
+ for (Map.Entry<String, Pair<String, Integer>> entry :
previousAddresses.entrySet()) {
+ if (!entry.getValue().equals(_mailboxAddresses.get(entry.getKey()))) {
+ retireChannel(entry.getKey(), entry.getValue());
}
}
_shuttingDownServers.clear();
@@ -123,10 +157,11 @@ public class ServerGrpcChannelBackoffResetHandler
implements InstanceConfigChang
if (config == null) {
return;
}
+ updateMailboxAddress(instance, config);
if (isShutdownInProgress(config)) {
_shuttingDownServers.add(instance);
} else if (_shuttingDownServers.remove(instance)) {
- resetBackoffForServer(instance, config);
+ refreshChannelToServer(instance, config);
}
}
@@ -145,24 +180,56 @@ public class ServerGrpcChannelBackoffResetHandler
implements InstanceConfigChang
config.getRecord().getStringField(CommonConstants.Helix.IS_SHUTDOWN_IN_PROGRESS,
"false"));
}
+ /// Records the server's current mailbox address, and retires the channel to
its previous one if it moved.
+ private void updateMailboxAddress(String instanceId, InstanceConfig config) {
+ Pair<String, Integer> previousAddress = recordMailboxAddress(instanceId,
config);
+ if (previousAddress != null &&
!previousAddress.equals(_mailboxAddresses.get(instanceId))) {
+ retireChannel(instanceId, previousAddress);
+ }
+ }
+
+ /// Records the server's current mailbox address, and returns its previous
one.
+ @Nullable
+ private Pair<String, Integer> recordMailboxAddress(String instanceId,
InstanceConfig config) {
+ Pair<String, Integer> address = getMailboxAddress(instanceId, config);
+ return address != null ? _mailboxAddresses.put(instanceId, address) :
_mailboxAddresses.remove(instanceId);
+ }
- private void resetBackoffForServer(String instanceId, InstanceConfig config)
{
+ private void retireChannel(String instanceId, Pair<String, Integer> address)
{
+ // Another server may have taken over the address
+ if (_mailboxAddresses.containsValue(address)) {
+ return;
+ }
+ if (_mailboxService.retireChannel(address.getLeft(), address.getRight())) {
+ LOGGER.info("Server {} left mailbox address {}:{}, shut down the channel
to it", instanceId, address.getLeft(),
+ address.getRight());
+ }
+ }
+
+ @Nullable
+ private static Pair<String, Integer> getMailboxAddress(String instanceId,
InstanceConfig config) {
int mailboxPort = config.getRecord().getIntField(
CommonConstants.Helix.Instance.MULTI_STAGE_QUERY_ENGINE_MAILBOX_PORT_KEY, -1);
if (mailboxPort <= 0) {
- LOGGER.debug("Server {} has no mailbox port configured, skipping backoff
reset", instanceId);
- return;
+ return null;
}
- String hostname;
try {
- hostname = ServerInstance.extractHostnameFromConfig(config);
+ return Pair.of(ServerInstance.extractHostnameFromConfig(config),
mailboxPort);
} catch (Exception e) {
- LOGGER.warn("Could not extract hostname for server: {}, skipping backoff
reset", instanceId, e);
+ LOGGER.warn("Could not extract hostname for server: {}", instanceId, e);
+ return null;
+ }
+ }
+
+ private void refreshChannelToServer(String instanceId, InstanceConfig
config) {
+ Pair<String, Integer> address = getMailboxAddress(instanceId, config);
+ if (address == null) {
+ LOGGER.debug("Server {} has no mailbox address, skipping channel
refresh", instanceId);
return;
}
- if (_mailboxService.resetConnectBackoff(hostname, mailboxPort)) {
- LOGGER.info("Server {} completed startup, reset mailbox channel backoff
to {}:{}", instanceId, hostname,
- mailboxPort);
+ if (_mailboxService.refreshChannel(address.getLeft(), address.getRight()))
{
+ LOGGER.info("Server {} completed startup, refreshed mailbox channel to
{}:{}", instanceId, address.getLeft(),
+ address.getRight());
}
}
}
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandlerTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
similarity index 55%
rename from
pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandlerTest.java
rename to
pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
index 36ae9b6b0c8..3fddb06e72b 100644
---
a/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerGrpcChannelBackoffResetHandlerTest.java
+++
b/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
@@ -40,7 +40,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-public class ServerGrpcChannelBackoffResetHandlerTest {
+public class ServerMailboxChannelRefreshHandlerTest {
private static final String CLUSTER_NAME = "testCluster";
private static final String SELF_INSTANCE_ID = "Server_selfhost_8000";
private static final String OTHER_SERVER_ID = "Server_otherhost_8000";
@@ -58,12 +58,12 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
@Mock
private MailboxService _mailboxService;
- private ServerGrpcChannelBackoffResetHandler _handler;
+ private ServerMailboxChannelRefreshHandler _handler;
@BeforeMethod
public void setUp() {
_mocks = MockitoAnnotations.openMocks(this);
- _handler = new ServerGrpcChannelBackoffResetHandler(_helixAdmin,
CLUSTER_NAME, SELF_INSTANCE_ID, _mailboxService);
+ _handler = new ServerMailboxChannelRefreshHandler(_helixAdmin,
CLUSTER_NAME, SELF_INSTANCE_ID, _mailboxService);
}
@AfterMethod
@@ -73,22 +73,22 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
}
@Test
- public void testResetsBackoffWhenServerCompletesStartup() {
+ public void testRefreshesChannelWhenServerCompletesStartup() {
// First callback: server has IS_SHUTDOWN_IN_PROGRESS=true (starting up)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, true));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
// Second callback: server has IS_SHUTDOWN_IN_PROGRESS=false (startup
complete)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService).resetConnectBackoff(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
@Test
- public void testDoesNotResetOnFirstCallbackForHealthyServer() {
+ public void testDoesNotRefreshOnFirstCallbackForHealthyServer() {
// First callback: server is healthy (IS_SHUTDOWN_IN_PROGRESS=false)
// This should NOT trigger a reset because we haven't seen it in
shutting-down state
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
@@ -96,11 +96,11 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
}
@Test
- public void testDoesNotResetForServerStillShuttingDown() {
+ public void testDoesNotRefreshForServerStillShuttingDown() {
// First callback: server is shutting down
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, true));
@@ -109,28 +109,28 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
// Second callback: server still shutting down
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
}
@Test
- public void testDoesNotResetForSelfInstance() {
+ public void testDoesNotRefreshForSelfInstance() {
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(SELF_INSTANCE_ID));
verify(_helixAdmin, never()).getInstanceConfig(any(), any());
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
}
@Test
- public void testDoesNotResetForBrokerInstance() {
+ public void testDoesNotRefreshForBrokerInstance() {
String brokerId = "Broker_brokerhost_8000";
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(brokerId));
verify(_helixAdmin, never()).getInstanceConfig(any(), any());
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
}
@Test
- public void testDoesNotResetForServerWithoutMailboxPort() {
+ public void testDoesNotRefreshForServerWithoutMailboxPort() {
// First callback: server shutting down (no mailbox port)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST, -1,
true));
@@ -141,32 +141,32 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST, -1,
false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
}
@Test
- public void testResetsBackoffOnCrashRecovery() {
+ public void testRefreshesChannelOnCrashRecovery() {
// First callback: server is healthy (IS_SHUTDOWN_IN_PROGRESS=false)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
// Second callback: server crashed and is restarting
(IS_SHUTDOWN_IN_PROGRESS=true)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, true));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
// Third callback: server completed startup (IS_SHUTDOWN_IN_PROGRESS=false)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService).resetConnectBackoff(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
@Test
- public void testResetsBackoffOnGracefulRestart() {
+ public void testRefreshesChannelOnGracefulRestart() {
// First callback: server is healthy
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
@@ -179,13 +179,13 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
// Third callback: still shutting down / restarting
(IS_SHUTDOWN_IN_PROGRESS=true)
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
// Fourth callback: startup complete (IS_SHUTDOWN_IN_PROGRESS=false)
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService).resetConnectBackoff(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
@Test
@@ -197,13 +197,109 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, true));
_handler.onInstanceConfigChange(List.of(), createInitContext());
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
// Subsequent individual callback: server completes startup
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
.thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
_handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
- verify(_mailboxService).resetConnectBackoff(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ /// A server that comes back on another mailbox port (the default when ports
are not pinned) leaves a channel to its
+ /// old address that nothing will use again.
+ @Test
+ public void testRetiresChannelToOldAddressWhenServerMoves() {
+ when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
+ .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+ // A config change that keeps the address retires nothing
+ _handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
+ verify(_mailboxService, never()).retireChannel(any(), anyInt());
+
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT + 1, true));
+ _handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
+ verify(_mailboxService).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService, never()).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT + 1);
+ }
+
+ @Test
+ public void testRetiresChannelOfServerThatLeftTheCluster() {
+ when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
+ .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+
when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME)).thenReturn(List.of(SELF_INSTANCE_ID));
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_mailboxService).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ /// A config that cannot be read now does not mean that the server left: its
channel stays.
+ @Test
+ public void testDoesNotRetireChannelWhenInstanceConfigReadFails() {
+ when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
+ .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME,
OTHER_SERVER_ID)).thenThrow(new RuntimeException("ZK hiccup"));
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_mailboxService, never()).retireChannel(any(), anyInt());
+ }
+
+ /// The channel to an address must stay when another server now has that
address.
+ @Test
+ public void testKeepsChannelToAddressTakenOverByAnotherServer() {
+ String thirdServerId = "Server_thirdhost_8000";
+ when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
+ .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID,
thirdServerId));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, thirdServerId))
+ .thenReturn(createServerConfig(thirdServerId, "thirdhost",
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+ // The third server takes over the address of the other server, which
moves away
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, thirdServerId))
+ .thenReturn(createServerConfig(thirdServerId, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(thirdServerId));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, "newhost",
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
+
+ verify(_mailboxService).retireChannel("thirdhost",
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService, never()).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ /// A full scan applies every server's new address before it retires any, so
it keeps the channel to an address that
+ /// one server left and another one took over in the same scan, whichever of
the two it reads first.
+ @Test
+ public void testFullScanKeepsChannelToAddressTakenOverInTheSameScan() {
+ String thirdServerId = "Server_thirdhost_8000";
+ when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
+ .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID,
thirdServerId));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, thirdServerId))
+ .thenReturn(createServerConfig(thirdServerId, "thirdhost",
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+ // One scan reads the other server first: it moved away, and the third
server took over its address
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
+ .thenReturn(createServerConfig(OTHER_SERVER_ID, "newhost",
OTHER_SERVER_MAILBOX_PORT, false));
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, thirdServerId))
+ .thenReturn(createServerConfig(thirdServerId, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false));
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+
+ verify(_mailboxService).retireChannel("thirdhost",
OTHER_SERVER_MAILBOX_PORT);
+ verify(_mailboxService, never()).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
private InstanceConfig createServerConfig(String instanceId, String
hostname, int mailboxPort,
@@ -234,7 +330,7 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
// Simulate a FINALIZE notification where pathChanged is null. The
listener should ignore this.
_handler.onInstanceConfigChange(List.of(),
createFinalizeContextWithNullPath());
- verify(_mailboxService, never()).resetConnectBackoff(any(), anyInt());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
verify(_helixAdmin, never()).getInstancesInCluster(any());
}
@@ -245,6 +341,13 @@ public class ServerGrpcChannelBackoffResetHandlerTest {
return context;
}
+ private NotificationContext createChildChangeContext() {
+ NotificationContext context = new NotificationContext(_helixManager);
+ context.setType(NotificationContext.Type.CALLBACK);
+ context.setIsChildChange(true);
+ return context;
+ }
+
private NotificationContext createInitContext() {
NotificationContext context = new NotificationContext(_helixManager);
context.setType(NotificationContext.Type.INIT);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]