This is an automated email from the ASF dual-hosted git repository.
xiangfu0 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 bfdb305e148 Skip mailbox config scans for non-server membership
changes (#20107)
bfdb305e148 is described below
commit bfdb305e148ff4ccb1ac0bee3062aedb55ee0dcc
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Oct 30 21:30:53 2026 -0700
Skip mailbox config scans for non-server membership changes (#20107)
* Skip mailbox config scans for non-server membership changes
Cache participant names so minion membership changes do not reread all
server configs. Preserve initialization, server membership scans, and retries
after failed reads.
Reproduction: initialize with a server, then add and remove a minion. The
regression test previously observed three server-config reads instead of one.
* Preserve mailbox recovery when skipping membership scans
Check a coherent parent generation and child version before skipping
non-server membership changes. Invalidate the snapshot after failed reads and
retain known shutdown state while retrying.
Reproduction: coalesce a same-name server recreation with a minion join, or
fail a readiness config read before a minion membership callback. Both cases
previously missed the channel refresh.
* Simplify mailbox membership snapshot handling
Use an immutable cache entry and keep the skip decision and snapshot
publication together. Preserve all recovery and reconciliation safeguards.
How to reproduce:
Run ServerMailboxChannelRefreshHandlerTest. Server add/remove snapshots now
advance cversion from 10 to 11 so they exercise the server-type guard.
---
.../helix/ServerMailboxChannelRefreshHandler.java | 81 +++++++-
.../ServerMailboxChannelRefreshHandlerTest.java | 215 +++++++++++++++++++--
2 files changed, 275 insertions(+), 21 deletions(-)
diff --git
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
index 331d88e12d9..578bd5476bc 100644
---
a/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
+++
b/pinot-server/src/main/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandler.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.server.starter.helix;
+import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -25,16 +26,21 @@ import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
+import org.apache.helix.AccessOption;
+import org.apache.helix.BaseDataAccessor;
import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixDataAccessor;
import org.apache.helix.NotificationContext;
import org.apache.helix.api.listeners.BatchMode;
import org.apache.helix.api.listeners.InstanceConfigChangeListener;
import org.apache.helix.api.listeners.PreFetch;
import org.apache.helix.model.InstanceConfig;
+import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.pinot.core.transport.ServerInstance;
import org.apache.pinot.query.mailbox.MailboxService;
import org.apache.pinot.spi.utils.CommonConstants;
import org.apache.pinot.spi.utils.InstanceTypeUtils;
+import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,6 +74,17 @@ public class ServerMailboxChannelRefreshHandler implements
InstanceConfigChangeL
private final String _selfInstanceId;
private final MailboxService _mailboxService;
+ // A coherent participant snapshot, invalidated whenever a membership or
instance config read fails.
+ @Nullable
+ private MembershipSnapshot _membershipSnapshot;
+
+ /// Immutable participant names and parent version from a coherent
membership read.
+ private record MembershipSnapshot(Set<String> instances, long parentCzxid,
int parentCversion) {
+ private MembershipSnapshot {
+ instances = Set.copyOf(instances);
+ }
+ }
+
// 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
refresh its channel.
private final Set<String> _shuttingDownServers = new HashSet<>();
@@ -90,10 +107,10 @@ public class ServerMailboxChannelRefreshHandler implements
InstanceConfigChangeL
// INIT: first callback when the listener is registered (full cluster
snapshot).
// isChildChange: an instance ZK node was added or removed under
/CONFIGS/PARTICIPANT.
- // Both require a full scan to rebuild _shuttingDownServers from the
current cluster state.
+ // INIT always scans; child changes scan only if they could affect servers.
NotificationContext.Type type = context.getType();
if (type == NotificationContext.Type.INIT || context.getIsChildChange()) {
- handleFullScan();
+ handleMembershipChange(context);
} else if (type == NotificationContext.Type.CALLBACK) {
// An existing instance's config changed (e.g. IS_SHUTDOWN_IN_PROGRESS
toggled).
// pathChanged is the ZK path of the specific instance that changed.
@@ -103,16 +120,57 @@ public class ServerMailboxChannelRefreshHandler
implements InstanceConfigChangeL
}
}
- /// Full cluster scan: used on INIT (first callback) and when instances are
added/removed.
+ /// Scans on INIT and child changes, skipping membership changes confined to
non-server instances.
+ private void handleMembershipChange(NotificationContext context) {
+ MembershipSnapshot previousSnapshot = _membershipSnapshot;
+ _membershipSnapshot = null;
+ HelixDataAccessor dataAccessor =
context.getManager().getHelixDataAccessor();
+ BaseDataAccessor<ZNRecord> baseDataAccessor =
dataAccessor.getBaseDataAccessor();
+ String parentPath = dataAccessor.keyBuilder().instanceConfigs().getPath();
+ Set<String> instances;
+ Stat before;
+ Stat after;
+ try {
+ // Helix has no atomic child-names/Stat read. A changing parent makes
this snapshot unsafe to cache.
+ before = baseDataAccessor.getStat(parentPath, AccessOption.PERSISTENT);
+ instances = new HashSet<>(baseDataAccessor.getChildNames(parentPath,
AccessOption.PERSISTENT));
+ after = baseDataAccessor.getStat(parentPath, AccessOption.PERSISTENT);
+ } catch (Exception e) {
+ LOGGER.warn("Failed to read participant membership version, scanning
server configs", e);
+ before = null;
+ after = null;
+ instances = new
HashSet<>(_helixAdmin.getInstancesInCluster(_clusterName));
+ }
+ boolean stableSnapshot = before != null && after != null &&
before.getCzxid() == after.getCzxid()
+ && before.getCversion() == after.getCversion() && before.getPzxid() ==
after.getPzxid();
+ if (context.getType() != NotificationContext.Type.INIT && previousSnapshot
!= null && stableSnapshot) {
+ Set<String> changedInstances =
Sets.symmetricDifference(previousSnapshot.instances(), instances);
+ // cversion counts all child creates/deletes, including same-name server
recreation hidden by the net diff.
+ // Long subtraction conservatively rejects counter wrap/reset; czxid
identifies parent recreation.
+ if (after.getCzxid() == previousSnapshot.parentCzxid()
+ && (long) after.getCversion() - previousSnapshot.parentCversion() ==
changedInstances.size()
+ && !changedInstances.isEmpty()
+ && changedInstances.stream().noneMatch(InstanceTypeUtils::isServer))
{
+ _membershipSnapshot = new MembershipSnapshot(instances,
after.getCzxid(), after.getCversion());
+ return;
+ }
+ }
+ if (scanServerConfigs(instances) && stableSnapshot) {
+ _membershipSnapshot = new MembershipSnapshot(instances,
after.getCzxid(), after.getCversion());
+ }
+ }
+
+ /// Reconciles server configs, returning whether every config was read
successfully.
///
/// 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() {
+ private boolean scanServerConfigs(Set<String> instances) {
+ boolean allConfigsRead = true;
Set<String> currentShuttingDown = new HashSet<>();
Set<String> currentServers = new HashSet<>();
Map<String, Pair<String, Integer>> previousAddresses = new HashMap<>();
- for (String instance : _helixAdmin.getInstancesInCluster(_clusterName)) {
+ for (String instance : instances) {
if (!InstanceTypeUtils.isServer(instance) ||
instance.equals(_selfInstanceId)) {
continue;
}
@@ -120,6 +178,10 @@ public class ServerMailboxChannelRefreshHandler implements
InstanceConfigChangeL
currentServers.add(instance);
InstanceConfig config = getInstanceConfig(instance);
if (config == null) {
+ allConfigsRead = false;
+ if (_shuttingDownServers.contains(instance)) {
+ currentShuttingDown.add(instance);
+ }
continue;
}
Pair<String, Integer> previousAddress = recordMailboxAddress(instance,
config);
@@ -145,6 +207,7 @@ public class ServerMailboxChannelRefreshHandler implements
InstanceConfigChangeL
}
_shuttingDownServers.clear();
_shuttingDownServers.addAll(currentShuttingDown);
+ return allConfigsRead;
}
/// Handles a single instance config change by reading only the changed
instance's config
@@ -168,11 +231,15 @@ public class ServerMailboxChannelRefreshHandler
implements InstanceConfigChangeL
@Nullable
private InstanceConfig getInstanceConfig(String instance) {
try {
- return _helixAdmin.getInstanceConfig(_clusterName, instance);
+ InstanceConfig config = _helixAdmin.getInstanceConfig(_clusterName,
instance);
+ if (config != null) {
+ return config;
+ }
} catch (Exception e) {
LOGGER.warn("Failed to get instance config for: {}, skipping", instance,
e);
- return null;
}
+ _membershipSnapshot = null;
+ return null;
}
private static boolean isShutdownInProgress(InstanceConfig config) {
diff --git
a/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
b/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
index 3fddb06e72b..36ae0337396 100644
---
a/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
+++
b/pinot-server/src/test/java/org/apache/pinot/server/starter/helix/ServerMailboxChannelRefreshHandlerTest.java
@@ -18,24 +18,31 @@
*/
package org.apache.pinot.server.starter.helix;
-import java.util.Arrays;
import java.util.List;
+import org.apache.helix.AccessOption;
+import org.apache.helix.BaseDataAccessor;
import org.apache.helix.HelixAdmin;
+import org.apache.helix.HelixDataAccessor;
import org.apache.helix.HelixManager;
import org.apache.helix.NotificationContext;
+import org.apache.helix.PropertyKey;
import org.apache.helix.model.InstanceConfig;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.pinot.query.mailbox.MailboxService;
import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.zookeeper.data.Stat;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,6 +53,8 @@ public class ServerMailboxChannelRefreshHandlerTest {
private static final String OTHER_SERVER_ID = "Server_otherhost_8000";
private static final String OTHER_SERVER_HOST = "otherhost";
private static final int OTHER_SERVER_MAILBOX_PORT = 9000;
+ private static final String INSTANCE_CONFIGS_PATH =
+ new PropertyKey.Builder(CLUSTER_NAME).instanceConfigs().getPath();
private AutoCloseable _mocks;
@@ -55,6 +64,12 @@ public class ServerMailboxChannelRefreshHandlerTest {
@Mock
private HelixAdmin _helixAdmin;
+ @Mock
+ private HelixDataAccessor _helixDataAccessor;
+
+ @Mock
+ private BaseDataAccessor<ZNRecord> _baseDataAccessor;
+
@Mock
private MailboxService _mailboxService;
@@ -63,6 +78,9 @@ public class ServerMailboxChannelRefreshHandlerTest {
@BeforeMethod
public void setUp() {
_mocks = MockitoAnnotations.openMocks(this);
+ when(_helixManager.getHelixDataAccessor()).thenReturn(_helixDataAccessor);
+ when(_helixDataAccessor.keyBuilder()).thenReturn(new
PropertyKey.Builder(CLUSTER_NAME));
+
when(_helixDataAccessor.getBaseDataAccessor()).thenReturn(_baseDataAccessor);
_handler = new ServerMailboxChannelRefreshHandler(_helixAdmin,
CLUSTER_NAME, SELF_INSTANCE_ID, _mailboxService);
}
@@ -190,8 +208,7 @@ public class ServerMailboxChannelRefreshHandlerTest {
@Test
public void testInitCallbackDoesFullScanAndTracksState() {
- when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
- .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID));
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID));
// INIT callback: server is shutting down, triggers full cluster scan
when(_helixAdmin.getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID))
@@ -206,12 +223,163 @@ public class ServerMailboxChannelRefreshHandlerTest {
verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
+ @Test
+ public void testMinionMembershipChangesDoNotReadServerConfigs() {
+ String minionId = "Minion_minionhost_8000";
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 10, 1);
+ 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());
+
+ stubParticipantSnapshot(List.of(minionId, OTHER_SERVER_ID,
SELF_INSTANCE_ID), 11, 1);
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 12, 1);
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_helixAdmin, times(1)).getInstanceConfig(CLUSTER_NAME,
OTHER_SERVER_ID);
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
+
+ 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).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ @Test
+ public void testServerMembershipChangesStillScanConfigs() {
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID), 10, 1);
+ 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());
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 11, 1);
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_helixAdmin).getInstanceConfig(CLUSTER_NAME, 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());
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ @DataProvider(name = "ambiguousMembershipChanges")
+ public static Object[][] ambiguousMembershipChanges() {
+ return new Object[][]{
+ // A server delete/recreate and a minion join are coalesced into one
callback.
+ {10, 13, 1L, 1L, true},
+ // A recreated parent has an unrelated cversion, even if its delta
matches the minion join.
+ {10, 11, 1L, 2L, true},
+ // Integer wrap must not look like an unambiguous single child change.
+ {Integer.MAX_VALUE, Integer.MIN_VALUE, 1L, 1L, true},
+ // The parent changed while its child names were being read.
+ {10, 11, 1L, 1L, false}
+ };
+ }
+
+ @Test(dataProvider = "ambiguousMembershipChanges")
+ public void testAmbiguousMinionMembershipChangeStillTracksServerStartup(int
previousCversion, int cversion,
+ long previousCzxid, long czxid, boolean stableSnapshot) {
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID),
previousCversion, previousCzxid);
+ 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());
+
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID,
"Minion_minionhost_8000"), cversion, czxid);
+ if (!stableSnapshot) {
+ when(_baseDataAccessor.getStat(INSTANCE_CONFIGS_PATH,
AccessOption.PERSISTENT))
+ .thenReturn(participantStat(previousCversion, czxid, 2),
participantStat(cversion, czxid, 3));
+ }
+ 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(), createChildChangeContext());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
+
+ 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).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ @DataProvider(name = "retryScanFailure")
+ public static Object[][] retryScanFailure() {
+ return new Object[][]{{false}, {true}};
+ }
+
+ @Test(dataProvider = "retryScanFailure")
+ public void testFailedReadRetriesOnMinionMembershipChange(boolean
failRetryScan) {
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 10, 1);
+ 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());
+
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME,
OTHER_SERVER_ID)).thenThrow(new RuntimeException("ZK hiccup"));
+ _handler.onInstanceConfigChange(List.of(),
createCallbackContextForInstance(OTHER_SERVER_ID));
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
+
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID,
"Minion_minionhost_8000"), 11, 1);
+ if (failRetryScan) {
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID,
+ "Minion_minionhost_8000", "Minion_anotherhost_8000"), 12, 1);
+ }
+ doReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, false))
+ .when(_helixAdmin).getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID);
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_mailboxService).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ @Test
+ public void testFailedInitReadRetriesOnMinionMembershipChange() {
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 10, 1);
+ when(_helixAdmin.getInstanceConfig(CLUSTER_NAME,
OTHER_SERVER_ID)).thenThrow(new RuntimeException("ZK hiccup"));
+ _handler.onInstanceConfigChange(List.of(), createInitContext());
+
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID,
"Minion_minionhost_8000"), 11, 1);
+ doReturn(createServerConfig(OTHER_SERVER_ID, OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT, true))
+ .when(_helixAdmin).getInstanceConfig(CLUSTER_NAME, OTHER_SERVER_ID);
+ _handler.onInstanceConfigChange(List.of(), createChildChangeContext());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
+
+ 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).refreshChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
+ }
+
+ @DataProvider(name = "metadataReadFailure")
+ public static Object[][] metadataReadFailure() {
+ return new Object[][]{{false}, {true}};
+ }
+
+ @Test(dataProvider = "metadataReadFailure")
+ public void
testMembershipMetadataReadFailureStillTracksServerStartup(boolean
throwOnStatRead) {
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 10, 1);
+ 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());
+
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID,
"Minion_minionhost_8000"), 11, 1);
+ // Missing metadata cannot prove that the minion join was the only change.
+ if (throwOnStatRead) {
+ when(_baseDataAccessor.getStat(INSTANCE_CONFIGS_PATH,
AccessOption.PERSISTENT))
+ .thenThrow(new RuntimeException("ZK hiccup"));
+ } else {
+ when(_baseDataAccessor.getStat(INSTANCE_CONFIGS_PATH,
AccessOption.PERSISTENT)).thenReturn(null);
+ }
+ 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(), createChildChangeContext());
+ verify(_mailboxService, never()).refreshChannel(any(), anyInt());
+
+ 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).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));
+ stubParticipantSnapshot(List.of(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());
@@ -229,13 +397,12 @@ public class ServerMailboxChannelRefreshHandlerTest {
@Test
public void testRetiresChannelOfServerThatLeftTheCluster() {
- when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
- .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID));
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID, OTHER_SERVER_ID), 10, 1);
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));
+ stubParticipantSnapshot(List.of(SELF_INSTANCE_ID), 11, 1);
_handler.onInstanceConfigChange(List.of(), createChildChangeContext());
verify(_mailboxService).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
@@ -243,8 +410,7 @@ public class ServerMailboxChannelRefreshHandlerTest {
/// 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));
+ stubParticipantSnapshot(List.of(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());
@@ -258,8 +424,7 @@ public class ServerMailboxChannelRefreshHandlerTest {
@Test
public void testKeepsChannelToAddressTakenOverByAnotherServer() {
String thirdServerId = "Server_thirdhost_8000";
- when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
- .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID,
thirdServerId));
+ stubParticipantSnapshot(List.of(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))
@@ -283,8 +448,7 @@ public class ServerMailboxChannelRefreshHandlerTest {
@Test
public void testFullScanKeepsChannelToAddressTakenOverInTheSameScan() {
String thirdServerId = "Server_thirdhost_8000";
- when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME))
- .thenReturn(Arrays.asList(SELF_INSTANCE_ID, OTHER_SERVER_ID,
thirdServerId));
+ stubParticipantSnapshot(List.of(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))
@@ -302,6 +466,27 @@ public class ServerMailboxChannelRefreshHandlerTest {
verify(_mailboxService, never()).retireChannel(OTHER_SERVER_HOST,
OTHER_SERVER_MAILBOX_PORT);
}
+ private void stubParticipantSnapshot(List<String> instances) {
+ stubParticipantSnapshot(instances, 0, 1);
+ }
+
+ private void stubParticipantSnapshot(List<String> instances, int cversion,
long czxid) {
+ // The Admin name list is the fallback when the versioned membership read
throws.
+
when(_helixAdmin.getInstancesInCluster(CLUSTER_NAME)).thenReturn(instances);
+ when(_baseDataAccessor.getChildNames(INSTANCE_CONFIGS_PATH,
AccessOption.PERSISTENT)).thenReturn(instances);
+ when(_baseDataAccessor.getStat(INSTANCE_CONFIGS_PATH,
AccessOption.PERSISTENT))
+ .thenReturn(participantStat(cversion, czxid, instances.size()));
+ }
+
+ private static Stat participantStat(int cversion, long czxid, int
numChildren) {
+ Stat stat = new Stat();
+ stat.setCversion(cversion);
+ stat.setCzxid(czxid);
+ stat.setPzxid(100L + Integer.toUnsignedLong(cversion));
+ stat.setNumChildren(numChildren);
+ return stat;
+ }
+
private InstanceConfig createServerConfig(String instanceId, String
hostname, int mailboxPort,
boolean shutdownInProgress) {
ZNRecord record = new ZNRecord(instanceId);
@@ -332,6 +517,8 @@ public class ServerMailboxChannelRefreshHandlerTest {
verify(_mailboxService, never()).refreshChannel(any(), anyInt());
verify(_helixAdmin, never()).getInstancesInCluster(any());
+ verify(_baseDataAccessor, never()).getStat(any(), anyInt());
+ verify(_baseDataAccessor, never()).getChildNames(any(), anyInt());
}
private NotificationContext createFinalizeContextWithNullPath() {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]