You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by ji...@apache.org on 2018/06/19 17:36:44 UTC

[geode] branch develop updated: GEODE-5284: Add testing surrounding MBean persistence during member failure and reconnection. (#2066)

This is an automated email from the ASF dual-hosted git repository.

jinmeiliao pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 82bc8f9  GEODE-5284: Add testing surrounding MBean persistence during member failure and reconnection. (#2066)
82bc8f9 is described below

commit 82bc8f9cb82806b6e5b5e3a91c382d8372eade00
Author: Helena Bales <hb...@pivotal.io>
AuthorDate: Tue Jun 19 10:36:40 2018 -0700

    GEODE-5284: Add testing surrounding MBean persistence during member failure and reconnection. (#2066)
    
    * Add null-check to ConfigurationChangeListener to avoid NPE when a locator joins.
    * Add withPort method to MemberStarterRule to specify membership port
    * Add testing associated with MemberStarterRule addition
    * Ensure cleanup of members in MemberStarterRuleIntegrationTest.
    * Improve names and extract helper methods
    * use the methods provided by the starter rules to force disconnect and
    to reconnect servers and locators. Adds a parameter in InternalLocator
    to track the status of the reconnect. Adds export of cache xml for
    reconnect so that the server is able to restore its state following
    disconnect.
    
    Co-Authored-by: Helena Bales <hb...@pivotal.io>
    Co-Authored-by: Dale Emery <da...@dhemery.com>
    Co-Authored-by: Patrick Rhomberg <pr...@pivotal.io>
    Co-Authored-By: Jinmei Liao <ji...@pivotal.io>
---
 .../distributed/internal/InternalLocator.java      |   7 +
 .../callbacks/ConfigurationChangeListener.java     |  12 +-
 .../internal/security/MBeanServerWrapper.java      |   1 -
 .../membership/gms/MembershipManagerHelper.java    |   3 +-
 .../management/JMXMBeanReconnectDUnitTest.java     | 305 +++++++++++++++++++++
 .../apache/geode/test/dunit/rules/MemberVM.java    |  27 ++
 .../tests/MemberStarterRuleIntegrationTest.java    |  77 ++++++
 .../dunit/rules/tests/MemberStarterRuleTest.java   |   9 +
 .../geode/test/junit/rules/LocatorStarterRule.java |   5 +-
 .../geode/test/junit/rules/MemberStarterRule.java  |   7 +-
 .../geode/test/junit/rules/ServerStarterRule.java  |   4 +-
 .../apache/geode/test/junit/rules/VMProvider.java  |   2 +-
 12 files changed, 449 insertions(+), 10 deletions(-)

diff --git a/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalLocator.java b/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalLocator.java
index 8b78b80..2ed7b9a 100644
--- a/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalLocator.java
+++ b/geode-core/src/main/java/org/apache/geode/distributed/internal/InternalLocator.java
@@ -166,6 +166,8 @@ public class InternalLocator extends Locator implements ConnectListener {
    */
   private volatile boolean stoppedForReconnect;
 
+  private volatile boolean reconnected;
+
   /**
    * whether the locator was stopped during forced-disconnect processing
    */
@@ -936,6 +938,7 @@ public class InternalLocator extends Locator implements ConnectListener {
           if (!restarted) {
             stoppedForReconnect = false;
           }
+          reconnected = restarted;
         }
         InternalLocator.this.restartThread = null;
       }
@@ -944,6 +947,10 @@ public class InternalLocator extends Locator implements ConnectListener {
     this.restartThread.start();
   }
 
+  public boolean isReconnected() {
+    return reconnected;
+  }
+
   /**
    * reconnects the locator to a restarting DistributedSystem. If quorum checks are enabled this
    * will start peer location services before a distributed system is available if the quorum check
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/configuration/callbacks/ConfigurationChangeListener.java b/geode-core/src/main/java/org/apache/geode/management/internal/configuration/callbacks/ConfigurationChangeListener.java
index 71d7005..64e9724 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/configuration/callbacks/ConfigurationChangeListener.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/configuration/callbacks/ConfigurationChangeListener.java
@@ -17,7 +17,6 @@ package org.apache.geode.management.internal.configuration.callbacks;
 import java.io.File;
 import java.io.IOException;
 import java.util.HashSet;
-import java.util.Optional;
 import java.util.Set;
 
 import org.apache.commons.io.FileUtils;
@@ -92,6 +91,10 @@ public class ConfigurationChangeListener extends CacheListenerAdapter<String, Co
     }
 
     String triggerMemberId = (String) event.getCallbackArgument();
+    if (triggerMemberId == null || newJars.isEmpty()) {
+      return;
+    }
+
     DistributedMember locator = getDistributedMember(triggerMemberId);
     for (String jarAdded : newJars) {
       try {
@@ -106,8 +109,9 @@ public class ConfigurationChangeListener extends CacheListenerAdapter<String, Co
     Set<DistributedMember> locators = new HashSet<>(
         cache.getDistributionManager().getAllHostedLocatorsWithSharedConfiguration().keySet());
 
-    Optional<DistributedMember> locator =
-        locators.stream().filter(x -> x.getId().equals(memberName)).findFirst();
-    return locator.get();
+    return locators.stream()
+        .filter(x -> x.getId().equals(memberName))
+        .findFirst()
+        .orElse(null);
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/security/MBeanServerWrapper.java b/geode-core/src/main/java/org/apache/geode/management/internal/security/MBeanServerWrapper.java
index 549e89f..24f0dbe 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/security/MBeanServerWrapper.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/security/MBeanServerWrapper.java
@@ -61,7 +61,6 @@ import org.apache.geode.security.ResourcePermission.Target;
  */
 public class MBeanServerWrapper implements MBeanServerForwarder {
 
-  // TODO: make volatile or verify this is thread-safe
   private MBeanServer mbs;
 
   private final SecurityService securityService;
diff --git a/geode-core/src/test/java/org/apache/geode/distributed/internal/membership/gms/MembershipManagerHelper.java b/geode-core/src/test/java/org/apache/geode/distributed/internal/membership/gms/MembershipManagerHelper.java
index f08c05d..09169f2 100644
--- a/geode-core/src/test/java/org/apache/geode/distributed/internal/membership/gms/MembershipManagerHelper.java
+++ b/geode-core/src/test/java/org/apache/geode/distributed/internal/membership/gms/MembershipManagerHelper.java
@@ -124,10 +124,11 @@ public class MembershipManagerHelper {
 
   public static void crashDistributedSystem(final DistributedSystem msys) {
     msys.getLogWriter().info("crashing distributed system: " + msys);
+    GMSMembershipManager mgr = ((GMSMembershipManager) getMembershipManager(msys));
+    mgr.saveCacheXmlForReconnect(false);
     MembershipManagerHelper.inhibitForcedDisconnectLogging(true);
     MembershipManagerHelper.beSickMember(msys);
     MembershipManagerHelper.playDead(msys);
-    GMSMembershipManager mgr = ((GMSMembershipManager) getMembershipManager(msys));
     mgr.forceDisconnect("for testing");
     // wait at most 10 seconds for system to be disconnected
     Awaitility.await().pollInterval(1, TimeUnit.SECONDS).until(() -> !msys.isConnected());
diff --git a/geode-core/src/test/java/org/apache/geode/management/JMXMBeanReconnectDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/JMXMBeanReconnectDUnitTest.java
new file mode 100644
index 0000000..2bf49c7
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/management/JMXMBeanReconnectDUnitTest.java
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.management;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.awaitility.Awaitility.waitAtMost;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.Cache;
+import org.apache.geode.distributed.ConfigurationProperties;
+import org.apache.geode.internal.AvailablePortHelper;
+import org.apache.geode.management.internal.SystemManagementService;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.DistributedTest;
+import org.apache.geode.test.junit.categories.JMXTest;
+import org.apache.geode.test.junit.rules.GfshCommandRule;
+import org.apache.geode.test.junit.rules.MBeanServerConnectionRule;
+
+@Category({DistributedTest.class, JMXTest.class})
+public class JMXMBeanReconnectDUnitTest {
+  private static final String LOCATOR_1_NAME = "locator-one";
+  private static final String LOCATOR_2_NAME = "locator-two";
+  private static final String REGION_PATH = "/test-region-1";
+  private static final int LOCATOR_1_VM_INDEX = 0;
+  private static final int LOCATOR_2_VM_INDEX = 1;
+  private static final int LOCATOR_COUNT = 2;
+  private static final int SERVER_1_VM_INDEX = 2;
+  private static final int SERVER_2_VM_INDEX = 3;
+  private static final int SERVER_COUNT = 2;
+
+  private int locator1JmxPort, locator2JmxPort;
+
+  private MemberVM locator1, locator2, server1, server2;
+
+  @Rule
+  public ClusterStartupRule lsRule = new ClusterStartupRule();
+
+  @Rule
+  public GfshCommandRule gfsh = new GfshCommandRule();
+
+  @Rule
+  public MBeanServerConnectionRule jmxConnectionRule = new MBeanServerConnectionRule();
+
+  @Before
+  public void before() throws Exception {
+    int[] ports = AvailablePortHelper.getRandomAvailableTCPPorts(LOCATOR_COUNT);
+    locator1JmxPort = ports[0];
+    locator2JmxPort = ports[1];
+
+    locator1 = lsRule.startLocatorVM(LOCATOR_1_VM_INDEX, locator1Properties());
+    locator2 = lsRule.startLocatorVM(LOCATOR_2_VM_INDEX, locator2Properties(), locator1.getPort());
+
+    server1 = lsRule.startServerVM(SERVER_1_VM_INDEX, locator1.getPort());
+    server2 = lsRule.startServerVM(SERVER_2_VM_INDEX, locator1.getPort());
+
+    gfsh.connectAndVerify(locator1);
+    gfsh.executeAndAssertThat(
+        "create region --type=REPLICATE --name=" + REGION_PATH + " --enable-statistics=true")
+        .statusIsSuccess();
+
+    locator1.waitTillRegionsAreReadyOnServers(REGION_PATH, SERVER_COUNT);
+    waitForLocatorsToAgreeOnMembership();
+  }
+
+  @Test
+  public void testLocalBeans_MaintainServerAndCrashLocator() {
+    List<String> initialServerBeans = canonicalBeanNamesFor(server1);
+
+    locator1.forceDisconnectMember();
+
+    List<String> intermediateServerBeans = canonicalBeanNamesFor(server1);
+
+    assertThat(intermediateServerBeans)
+        .containsExactlyElementsOf(initialServerBeans);
+
+    locator1.waitTilLocatorFullyReconnected();
+
+    List<String> finalServerBeans = canonicalBeanNamesFor(server1);
+
+    assertThat(finalServerBeans)
+        .containsExactlyElementsOf(initialServerBeans);
+  }
+
+  @Test
+  public void testLocalBeans_MaintainLocatorAndCrashServer() {
+    List<String> initialLocatorBeans = canonicalBeanNamesFor(locator1);
+
+    server1.forceDisconnectMember();
+
+    List<String> intermediateLocatorBeans = canonicalBeanNamesFor(locator1);
+
+    assertThat(intermediateLocatorBeans)
+        .containsExactlyElementsOf(initialLocatorBeans);
+
+    server1.waitTilServerFullyReconnected();
+    locator1.waitTillRegionsAreReadyOnServers(REGION_PATH, SERVER_COUNT);
+
+    List<String> finalLocatorBeans = canonicalBeanNamesFor(locator1);
+
+    assertThat(finalLocatorBeans)
+        .containsExactlyElementsOf(initialLocatorBeans);
+  }
+
+  @Test
+  public void testRemoteBeanKnowledge_MaintainServerAndCrashLocator() throws IOException {
+    List<ObjectName> initialLocator1GemfireBeans =
+        getFederatedGemfireBeansFrom(locator1);
+    List<ObjectName> initialLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    assertThat(initialLocator1GemfireBeans)
+        .containsExactlyElementsOf(initialLocator2GemfireBeans);
+
+    locator1.forceDisconnectMember();
+
+    List<ObjectName> intermediateLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    List<ObjectName> locatorBeansExcludingStoppedMember = initialLocator2GemfireBeans.stream()
+        .filter(excludingBeansFor(LOCATOR_1_NAME)).collect(toList());
+
+    assertThat(intermediateLocator2GemfireBeans)
+        .containsExactlyElementsOf(locatorBeansExcludingStoppedMember);
+
+    locator1.waitTilLocatorFullyReconnected();
+    waitForLocatorsToAgreeOnMembership();
+
+    List<ObjectName> finalLocator1GemfireBeans =
+        getFederatedGemfireBeansFrom(locator1);
+    List<ObjectName> finalLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    assertSoftly(softly -> {
+      softly.assertThat(finalLocator1GemfireBeans)
+          .containsExactlyElementsOf(finalLocator2GemfireBeans);
+      softly.assertThat(finalLocator1GemfireBeans)
+          .containsExactlyElementsOf(initialLocator2GemfireBeans);
+    });
+  }
+
+  @Test
+  public void testRemoteBeanKnowledge_MaintainLocatorAndCrashServer()
+      throws IOException {
+    List<ObjectName> initialLocator1GemfireBeans =
+        getFederatedGemfireBeansFrom(locator1);
+    List<ObjectName> initialLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    assertThat(initialLocator1GemfireBeans)
+        .containsExactlyElementsOf(initialLocator2GemfireBeans);
+
+    server1.forceDisconnectMember();
+
+    List<ObjectName> intermediateLocator1GemfireBeans =
+        getFederatedGemfireBeansFrom(locator1);
+    List<ObjectName> intermediateLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    List<ObjectName> locatorBeansExcludingStoppedMember = initialLocator1GemfireBeans.stream()
+        .filter(excludingBeansFor("server-" + SERVER_1_VM_INDEX))
+        .collect(toList());
+
+    assertSoftly(softly -> {
+      softly.assertThat(intermediateLocator2GemfireBeans)
+          .containsExactlyElementsOf(intermediateLocator1GemfireBeans);
+      softly.assertThat(intermediateLocator2GemfireBeans)
+          .containsExactlyElementsOf(locatorBeansExcludingStoppedMember);
+    });
+
+    server1.waitTilServerFullyReconnected();
+    locator1.waitTillRegionsAreReadyOnServers(REGION_PATH, SERVER_COUNT);
+    waitForLocatorsToAgreeOnMembership();
+
+    List<ObjectName> finalLocator1GemfireBeans =
+        getFederatedGemfireBeansFrom(locator1);
+    List<ObjectName> finalLocator2GemfireBeans =
+        getFederatedGemfireBeansFrom(locator2);
+
+    assertSoftly(softly -> {
+      softly.assertThat(finalLocator1GemfireBeans)
+          .containsExactlyElementsOf(finalLocator2GemfireBeans);
+      softly.assertThat(finalLocator1GemfireBeans)
+          .containsExactlyElementsOf(initialLocator2GemfireBeans);
+    });
+  }
+
+  private static List<ObjectName> getFederatedGemfireBeansFrom(MemberVM member)
+      throws IOException {
+    String url = jmxBeanLocalhostUrlString(member.getJmxPort());
+    MBeanServerConnection remoteMBS = connectToMBeanServer(url);
+    return getFederatedGemfireBeanObjectNames(remoteMBS);
+  }
+
+  private static MBeanServerConnection connectToMBeanServer(String url) throws IOException {
+    final JMXServiceURL serviceURL = new JMXServiceURL(url);
+    JMXConnector conn = JMXConnectorFactory.connect(serviceURL);
+    return conn.getMBeanServerConnection();
+  }
+
+  private static List<ObjectName> getFederatedGemfireBeanObjectNames(
+      MBeanServerConnection remoteMBS)
+      throws IOException {
+    Set<ObjectName> allBeans = remoteMBS.queryNames(null, null);
+    // Each locator will have a "Manager" bean that is a part of the above query,
+    // representing the ManagementAdapter.
+    // This bean is registered (and so included in its own queries),
+    // but *not* federated (and so is not included in another locator's bean queries).
+    // For the scope of this test, we do not consider these "service=Manager" beans.
+    return allBeans.stream()
+        .filter(b -> b.toString().contains("GemFire"))
+        .filter(b -> !b.toString().contains("service=Manager,type=Member,member=locator"))
+        .sorted()
+        .collect(toList());
+  }
+
+  private static List<String> canonicalBeanNamesFor(MemberVM member) {
+    return member.invoke(JMXMBeanReconnectDUnitTest::getLocalCanonicalBeanNames);
+  }
+
+  private static List<String> getLocalCanonicalBeanNames() {
+    Cache cache = ClusterStartupRule.getCache();
+    SystemManagementService service =
+        (SystemManagementService) ManagementService.getExistingManagementService(cache);
+    Map<ObjectName, Object> gfBeanMap = service.getJMXAdapter().getLocalGemFireMBean();
+    return gfBeanMap.keySet().stream()
+        .map(ObjectName::getCanonicalName)
+        .sorted()
+        .collect(toList());
+  }
+
+  private void waitForLocatorsToAgreeOnMembership() {
+    waitAtMost(1, MINUTES)
+        .until(
+            () -> {
+              int locator1BeanCount =
+                  getFederatedGemfireBeansFrom(locator1)
+                      .size();
+              int locator2BeanCount =
+                  getFederatedGemfireBeansFrom(locator2)
+                      .size();
+              return locator1BeanCount == locator2BeanCount;
+            });
+  }
+
+  private static Predicate<ObjectName> excludingBeansFor(String memberName) {
+    return b -> !b.getCanonicalName().contains("member=" + memberName);
+  }
+
+  private static String jmxBeanLocalhostUrlString(int port) {
+    return "service:jmx:rmi:///jndi/rmi://localhost"
+        + ":" + port + "/jmxrmi";
+  }
+
+  private Properties locator1Properties() {
+    Properties props = new Properties();
+    props.setProperty(ConfigurationProperties.JMX_MANAGER_HOSTNAME_FOR_CLIENTS, "localhost");
+    props.setProperty(ConfigurationProperties.JMX_MANAGER_PORT, "" + locator1JmxPort);
+    props.setProperty(ConfigurationProperties.NAME, LOCATOR_1_NAME);
+    props.setProperty(ConfigurationProperties.MAX_WAIT_TIME_RECONNECT, "5000");
+    return props;
+  }
+
+  private Properties locator2Properties() {
+    Properties props = new Properties();
+    props.setProperty(ConfigurationProperties.JMX_MANAGER_HOSTNAME_FOR_CLIENTS, "localhost");
+    props.setProperty(ConfigurationProperties.JMX_MANAGER_PORT, "" + locator2JmxPort);
+    props.setProperty(ConfigurationProperties.NAME, LOCATOR_2_NAME);
+    props.setProperty(ConfigurationProperties.LOCATORS, "localhost[" + locator1.getPort() + "]");
+    return props;
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/test/dunit/rules/MemberVM.java b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/MemberVM.java
index 7f2c095..eef2990 100644
--- a/geode-core/src/test/java/org/apache/geode/test/dunit/rules/MemberVM.java
+++ b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/MemberVM.java
@@ -14,11 +14,18 @@
  */
 package org.apache.geode.test.dunit.rules;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import java.io.File;
 import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.io.FileUtils;
+import org.awaitility.Awaitility;
 
+import org.apache.geode.distributed.internal.InternalDistributedSystem;
+import org.apache.geode.distributed.internal.InternalLocator;
+import org.apache.geode.internal.cache.InternalCache;
 import org.apache.geode.test.dunit.VM;
 import org.apache.geode.test.junit.rules.Locator;
 import org.apache.geode.test.junit.rules.Member;
@@ -102,6 +109,26 @@ public class MemberVM extends VMProvider implements Member {
     vm.invoke(() -> ClusterStartupRule.memberStarter.forceDisconnectMember());
   }
 
+  public void waitTilLocatorFullyReconnected() {
+    vm.invoke(() -> Awaitility.waitAtMost(30, TimeUnit.SECONDS).until(() -> {
+      InternalLocator intLocator = InternalLocator.getLocator();
+      InternalCache cache = ClusterStartupRule.getCache();
+      return intLocator != null && cache != null && intLocator.getDistributedSystem().isConnected()
+          && intLocator.isReconnected();
+    }));
+  }
+
+  public void waitTilServerFullyReconnected() {
+    vm.invoke(() -> Awaitility.waitAtMost(30, SECONDS).until(() -> {
+      InternalDistributedSystem internalDistributedSystem =
+          InternalDistributedSystem.getConnectedInstance();
+      return internalDistributedSystem != null
+          && internalDistributedSystem.getCache() != null
+          && !internalDistributedSystem.getCache().getCacheServers().isEmpty();
+    }));
+
+  }
+
   /**
    * this should called on a locatorVM or a serverVM with jmxManager enabled
    */
diff --git a/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleIntegrationTest.java b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleIntegrationTest.java
new file mode 100644
index 0000000..359fcaf
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleIntegrationTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.test.dunit.rules.tests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.server.CacheServer;
+import org.apache.geode.distributed.internal.InternalLocator;
+import org.apache.geode.internal.AvailablePort;
+import org.apache.geode.test.junit.categories.IntegrationTest;
+import org.apache.geode.test.junit.rules.LocatorStarterRule;
+import org.apache.geode.test.junit.rules.ServerStarterRule;
+
+@Category(IntegrationTest.class)
+public class MemberStarterRuleIntegrationTest {
+
+
+  private LocatorStarterRule locator;
+  private ServerStarterRule server;
+
+  @After
+  public void cleanupAnyStartedMembers() {
+    if (locator != null) {
+      locator.after();
+    }
+    if (server != null) {
+      server.after();
+    }
+  }
+
+  @Test
+  public void testWithPortOnLocator() {
+    int targetPort = AvailablePort.getRandomAvailablePort(1);
+    locator = new LocatorStarterRule().withPort(targetPort).withAutoStart();
+    locator.before();
+
+    InternalLocator internalMember = locator.getLocator();
+
+    // This is the rule framework's port
+    assertThat(locator.getPort()).isEqualTo(targetPort);
+    // This is the actual, live member's port.
+    assertThat(internalMember.getPort()).isEqualTo(targetPort);
+
+  }
+
+  @Test
+  public void testWithPortOnServer() {
+    int targetPort = AvailablePort.getRandomAvailablePort(1);
+    server = new ServerStarterRule().withPort(targetPort).withAutoStart();
+    server.before();
+
+    CacheServer internalMember = server.getServer();
+
+    // This is the rule framework's port
+    assertThat(server.getPort()).isEqualTo(targetPort);
+    // This is the actual, live member's port.
+    assertThat(internalMember.getPort()).isEqualTo(targetPort);
+
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleTest.java b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleTest.java
index 2ba751b..30d83bd 100644
--- a/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleTest.java
+++ b/geode-core/src/test/java/org/apache/geode/test/dunit/rules/tests/MemberStarterRuleTest.java
@@ -68,6 +68,15 @@ public class MemberStarterRuleTest {
   }
 
   @Test
+  public void testWithPort() {
+    int targetPort = 12345;
+    locator = new LocatorStarterRule().withPort(targetPort);
+    locator.before();
+
+    assertThat(locator.getPort()).isEqualTo(targetPort);
+  }
+
+  @Test
   public void testUseRandomPortByDefault() {
     locator = new LocatorStarterRule();
     locator.before();
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/LocatorStarterRule.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/LocatorStarterRule.java
index f33e0eb..10e9a7a 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/LocatorStarterRule.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/LocatorStarterRule.java
@@ -77,11 +77,14 @@ public class LocatorStarterRule extends MemberStarterRule<LocatorStarterRule> im
   public void startLocator() {
     try {
       // this will start a jmx manager and admin rest service by default
-      locator = (InternalLocator) startLocatorAndDS(0, null, properties);
+      locator = (InternalLocator) startLocatorAndDS(memberPort, null, properties);
     } catch (IOException e) {
       throw new UncheckedIOException(e);
     }
+    // memberPort is by default zero, which translates to "randomly select an available port,"
+    // which is why it is updated here after being specified above.
     memberPort = locator.getPort();
+
     DistributionConfig config = locator.getConfig();
     jmxPort = config.getJmxManagerPort();
     httpPort = config.getHttpServicePort();
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/MemberStarterRule.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/MemberStarterRule.java
index 58e5b12..b42cba0 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/MemberStarterRule.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/MemberStarterRule.java
@@ -63,7 +63,7 @@ public abstract class MemberStarterRule<T> extends SerializableExternalResource
 
   protected transient TemporaryFolder temporaryFolder;
   protected File workingDir;
-  protected int memberPort = -1;
+  protected int memberPort = 0;
   protected int jmxPort = -1;
   protected int httpPort = -1;
 
@@ -105,6 +105,11 @@ public abstract class MemberStarterRule<T> extends SerializableExternalResource
     }
   }
 
+  public T withPort(int memberPort) {
+    this.memberPort = memberPort;
+    return (T) this;
+  }
+
   public T withWorkingDir(File workingDir) {
     this.workingDir = workingDir;
     if (workingDir != null) {
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/ServerStarterRule.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/ServerStarterRule.java
index 94dcb55..ca60fac 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/ServerStarterRule.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/ServerStarterRule.java
@@ -173,7 +173,9 @@ public class ServerStarterRule extends MemberStarterRule<ServerStarterRule> impl
     DistributionConfig config =
         ((InternalDistributedSystem) cache.getDistributedSystem()).getConfig();
     server = cache.addCacheServer();
-    server.setPort(0);
+    // memberPort is by default zero, which translates to "randomly select an available port,"
+    // which is why it is updated after this try block
+    server.setPort(memberPort);
     try {
       server.start();
     } catch (IOException e) {
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/rules/VMProvider.java b/geode-core/src/test/java/org/apache/geode/test/junit/rules/VMProvider.java
index f558b98..1b8f014 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/rules/VMProvider.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/rules/VMProvider.java
@@ -35,7 +35,7 @@ public abstract class VMProvider {
       ClusterStartupRule.stopElementInsideVM();
       MemberStarterRule.disconnectDSIfAny();
     });
-  };
+  }
 
   public boolean isClient() {
     return getVM().invoke(() -> {