This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new 7ad1d40488 Review "Deduplicate ... when merging static and dynamic
membership"
7ad1d40488 is described below
commit 7ad1d40488fae12bba94c83ad3ac17c78ab55b8d
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Oct 21 19:46:35 2026 +0100
Review "Deduplicate ... when merging static and dynamic membership"
Less of a review and more of a rewrite. Performing the merge in
getMembers() created all sorts of edge cases where different components
had different views of the current membership.
This approach ensures a consistent view at the cost of some complexity.
The short version is that if a node is receives a dynamic "member add"
notification for a static node it already knows about, the dynamic node
replaces the static one.
Co-authored by: Claude / Sonnet 5.5
Co-authored by: CoPilot / GPT-5.6 Sol
---
.../group/interceptors/LocalStrings.properties | 3 +
.../interceptors/StaticMembershipInterceptor.java | 281 ++++++++--
.../group/interceptors/TcpFailureDetector.java | 28 +
.../TestStaticMembershipInterceptor.java | 577 ++++++++++++++++-----
.../TestTcpFailureDetectorLivenessChecks.java | 65 +++
webapps/docs/changelog.xml | 20 +
6 files changed, 816 insertions(+), 158 deletions(-)
diff --git
a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
index fd9e1d6f9b..d80dbee32b 100644
--- a/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
+++ b/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties
@@ -89,10 +89,13 @@
nonBlockingCoordinator.waitForCoordMsgTimeout.tooSmall=Coordination message wait
orderInterceptor.messageAdded.sameCounter=Message added has the same counter,
synchronization bug. Disable the order interceptor
+staticMembershipInterceptor.duplicateStaticMember=The static member [{0}] has
the same host and ports as the static member [{1}] that is already configured
and is ignored. This is a configuration error.
+staticMembershipInterceptor.memberPortMismatch=The statically configured
member [{0}] and the dynamically discovered member [{1}] have the same host but
only some of the same ports. This is a configuration error, the two have not
been treated as the same node.
staticMembershipInterceptor.no.failureDetector=There is no TcpFailureDetector.
Automatic detection of static members does not work properly. By defining the
StaticMembershipInterceptor under the TcpFailureDetector, automatic detection
of the static members will work.
staticMembershipInterceptor.no.pingInterceptor=There is no TcpPingInterceptor.
The health check of static members does not work properly. By defining the
TcpPingInterceptor, the health check of static members will work.
staticMembershipInterceptor.sendLocalMember.failed=Local member notification
failed.
staticMembershipInterceptor.sendShutdown.failed=Shutdown notification failed.
+staticMembershipInterceptor.staticMemberPortMismatch=The static member [{0}]
has the same host as the static member [{1}] but only some of the same ports.
This is a configuration error, the two have been treated as different nodes.
tcpFailureDetector.already.disappeared=Verification complete. Member already
disappeared [{0}]
tcpFailureDetector.failureDetection.failed=Unable to perform failure detection
check, assuming member down. [{0}]
diff --git
a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java
b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java
index 8b2c2221ca..eabd56d55c 100644
---
a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java
+++
b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java
@@ -18,7 +18,9 @@ package org.apache.catalina.tribes.group.interceptors;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import org.apache.catalina.tribes.Channel;
@@ -61,6 +63,11 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
* List of static members configured for this interceptor.
*/
protected final ArrayList<Member> members = new ArrayList<>();
+ /**
+ * Static members that have been replaced by the dynamically discovered
member for the same node, keyed by the
+ * member that replaced them. Guarded by {@link #members}.
+ */
+ private final Map<Member,Member> replaced = new HashMap<>();
/**
* The local member for this interceptor.
*/
@@ -75,28 +82,67 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
/**
* Adds a static member to the membership list.
+ *
* @param member the member to add
*/
public void addStaticMember(Member member) {
+ List<Member> withdrawn = new ArrayList<>();
synchronized (members) {
- if (!members.contains(member)) {
+ if (!members.contains(member) && !isDuplicateStaticMember(member))
{
members.add(member);
+ if (getNext() != null) {
+ // the node may already have been discovered dynamically
+ for (Member dynamic : super.getMembers()) {
+ withdrawn.addAll(replaceConfiguredMember(dynamic));
+ }
+ }
+ }
+ }
+ // outside of the lock, as the failure detector notifies the
interceptors above
+ withdrawConfiguredMembers(withdrawn);
+ }
+
+ /**
+ * Checks if the static member to be added is for the same node as a
static member that has been configured before,
+ * including the ones that are currently replaced by a dynamically
discovered member. The same host and enabled
+ * ports with a different unique ID is a configuration error, and the
later entry is ignored since the node would
+ * otherwise receive every message twice. The same host with only some of
the same enabled ports is also reported as
+ * a configuration error, but both entries are kept. Must be called while
holding the lock on {@link #members}.
+ *
+ * @param member the static member to be added
+ *
+ * @return {@code true} if a static member for the same node has been
configured
+ */
+ private boolean isDuplicateStaticMember(Member member) {
+ List<Member> configured = new ArrayList<>(members);
+ configured.addAll(replaced.values());
+ for (Member other : configured) {
+ MemberMatch match = match(other, member);
+ if (match == MemberMatch.SAME_NODE) {
+
log.error(sm.getString("staticMembershipInterceptor.duplicateStaticMember",
member, other));
+ return true;
+ } else if (match == MemberMatch.PORT_MISMATCH) {
+
log.error(sm.getString("staticMembershipInterceptor.staticMemberPortMismatch",
member, other));
}
}
+ return false;
}
/**
* Removes a static member from the membership list.
+ *
* @param member the member to remove
*/
public void removeStaticMember(Member member) {
synchronized (members) {
members.remove(member);
+ replaced.values().remove(member);
}
}
/**
* Sets the local member for this interceptor.
+ *
* @param member the local member
*/
public void setLocalMember(Member member) {
@@ -142,55 +188,194 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
} else {
synchronized (members) {
Member[] others = super.getMembers();
- ArrayList<Member> result = new ArrayList<>(members.size() +
others.length);
- result.addAll(Arrays.asList(others));
- for (Member member : members) {
- // A statically configured node that is also discovered
- // dynamically would otherwise be returned twice and
- // receive every message sent to the full member list.
- // The two copies are only equal by Member.equals when the
- // static configuration pins the same unique id the node
- // announces, so the destination TCP endpoint is used to
- // detect the duplicate instead
- if (!containsDestination(result, member)) {
- result.add(member);
- }
+ Member[] result = new Member[members.size() + others.length];
+ System.arraycopy(others, 0, result, 0, others.length);
+ for (int i = 0; i < members.size(); i++) {
+ result[i + others.length] = members.get(i);
}
- Member[] sorted = result.toArray(new Member[0]);
- AbsoluteOrder.absoluteOrder(sorted);
- return sorted;
+ AbsoluteOrder.absoluteOrder(result);
+ return result;
} // sync
} // end if
}
/**
- * Checks whether the list of members already includes a member pointing
at the same TCP
- * destination as the given member. Two nodes cannot listen on the same
TCP port on the same
- * host, so host plus effective TCP port identifies the node,
independently of the unique id
- * that each copy happens to carry.
+ * Replaces the statically configured member for a node with the member
for the same node that was discovered
+ * dynamically. The two represent the same node when the host and all of
the enabled ports (TCP, secure and UDP) are
+ * the same. The unique ID of a dynamically discovered member is generated
when the node starts, so it can not be
+ * known by the static configuration, and without the replacement the node
would be present twice. If the host and
+ * only some of the enabled ports match, the static configuration is not
consistent with the node and is reported as
+ * a configuration error.
+ *
+ * @param dynamic the member that was discovered dynamically
*
- * @param list the members already collected for the result
- * @param member the member to look for
- * @return true if the list already contains the same TCP destination
+ * @return the replaced configured members, which have to be withdrawn by
the caller, outside of the lock on
+ * {@link #members}
*/
- private static boolean containsDestination(List<Member> list, Member
member) {
- int port = member.getPort() >= 0 ? member.getPort() :
member.getSecurePort();
- for (Member other : list) {
- int otherPort = other.getPort() >= 0 ? other.getPort() :
other.getSecurePort();
- if (otherPort == port && Arrays.equals(other.getHost(),
member.getHost())) {
- return true;
+ private List<Member> replaceConfiguredMember(Member dynamic) {
+ List<Member> withdrawn = new ArrayList<>();
+ synchronized (members) {
+ // The node may have been replaced earlier by a member that is no
longer current, for example when the
+ // node was restarted and now announces a new unique ID
+ Member previous = null;
+ Member previousConfigured = null;
+ for (Map.Entry<Member,Member> entry : replaced.entrySet()) {
+ MemberMatch match = match(entry.getValue(), dynamic);
+ if (match == MemberMatch.SAME_NODE) {
+ previous = entry.getKey();
+ previousConfigured = entry.getValue();
+ } else if (match == MemberMatch.PORT_MISMATCH) {
+ log.error(
+
sm.getString("staticMembershipInterceptor.memberPortMismatch",
entry.getValue(), dynamic));
+ }
+ }
+ if (previous != null) {
+ replaced.remove(previous);
+ replaced.put(dynamic, previousConfigured);
+ }
+ for (int i = 0; i < members.size();) {
+ Member configured = members.get(i);
+ MemberMatch match = match(configured, dynamic);
+ if (match == MemberMatch.SAME_NODE) {
+ members.remove(i);
+ replaced.put(dynamic, configured);
+ withdrawn.add(configured);
+ } else {
+ if (match == MemberMatch.PORT_MISMATCH) {
+
log.error(sm.getString("staticMembershipInterceptor.memberPortMismatch",
configured, dynamic));
+ }
+ i++;
+ }
}
}
- return false;
+ return withdrawn;
+ }
+
+ /**
+ * Withdraws configured members that have been replaced by a dynamically
discovered member from the failure detector
+ * above this interceptor. The failure detector has to forget them, as it
would otherwise keep them as suspects and
+ * add them again next to the discovered member, and it tells the
interceptors above it that they disappeared.
+ * Without a failure detector the configured members are not withdrawn.
+ *
+ * @param withdrawn the configured members to withdraw
+ */
+ private void withdrawConfiguredMembers(List<Member> withdrawn) {
+ if (withdrawn.isEmpty()) {
+ return;
+ }
+ TcpFailureDetector failureDetector = getFailureDetector();
+ if (failureDetector != null) {
+ for (Member configured : withdrawn) {
+ failureDetector.forgetMember(configured);
+ }
+ }
+ }
+
+ /**
+ * Finds the closest failure detector above this interceptor.
+ *
+ * @return the failure detector, or {@code null} if there is none
+ */
+ private TcpFailureDetector getFailureDetector() {
+ ChannelInterceptor prev = getPrevious();
+ while (prev != null) {
+ if (prev instanceof TcpFailureDetector failureDetector) {
+ return failureDetector;
+ }
+ prev = prev.getPrevious();
+ }
+ return null;
+ }
+
+ private enum MemberMatch {
+ /** Different nodes. */
+ DIFFERENT_NODE,
+ /** The host and all ports match. */
+ SAME_NODE,
+ /** The host matches and some, but not all, of the ports match. */
+ PORT_MISMATCH
+ }
+
+ /*
+ * Only ports that are used by at least one of the members are compared. A
port that neither member uses is -1 on
+ * both, and must not make two nodes with different TCP ports on the same
host look partially identical.
+ */
+ private static MemberMatch match(Member configured, Member dynamic) {
+ if (!Arrays.equals(configured.getHost(), dynamic.getHost())) {
+ return MemberMatch.DIFFERENT_NODE;
+ }
+ int used = 0;
+ int matching = 0;
+ int[][] ports =
+ { { configured.getPort(), dynamic.getPort() }, {
configured.getSecurePort(), dynamic.getSecurePort() },
+ { configured.getUdpPort(), dynamic.getUdpPort() } };
+ for (int[] pair : ports) {
+ if (pair[0] >= 0 || pair[1] >= 0) {
+ used++;
+ if (pair[0] == pair[1]) {
+ matching++;
+ }
+ }
+ }
+ if (used == 0 || matching == 0) {
+ return MemberMatch.DIFFERENT_NODE;
+ } else if (matching == used) {
+ return MemberMatch.SAME_NODE;
+ } else {
+ return MemberMatch.PORT_MISMATCH;
+ }
+ }
+
+ @Override
+ public void memberAdded(Member member) {
+ // The lock on members is only held to update the state. The failure
detector verifies the member over the
+ // network and notifies the interceptors above, which must not happen
while holding the lock.
+ List<Member> withdrawn;
+ boolean replacing;
+ synchronized (members) {
+ withdrawn = replaceConfiguredMember(member);
+ replacing = !withdrawn.isEmpty() || replaced.containsKey(member);
+ }
+ // the configured member is withdrawn before the member replacing it
is announced
+ withdrawConfiguredMembers(withdrawn);
+ super.memberAdded(member);
+ if (replacing) {
+ // The member may have disappeared while it was being announced,
and the disappearance may have been
+ // handled before the announcement. The failure detector would
then keep a member that is gone.
+ boolean stale;
+ synchronized (members) {
+ stale = !replaced.containsKey(member);
+ }
+ if (stale) {
+ TcpFailureDetector failureDetector = getFailureDetector();
+ if (failureDetector != null) {
+ failureDetector.forgetMember(member);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void memberDisappeared(Member member) {
+ synchronized (members) {
+ // the dynamic member is gone, the node stays a member as it was
configured
+ Member configured = replaced.remove(member);
+ if (configured != null && !members.contains(configured)) {
+ members.add(configured);
+ }
+ }
+ super.memberDisappeared(member);
}
@Override
public Member getMember(Member mbr) {
- if (members.contains(mbr)) {
- return members.get(members.indexOf(mbr));
- } else {
- return super.getMember(mbr);
+ synchronized (members) {
+ int index = members.indexOf(mbr);
+ if (index >= 0) {
+ return members.get(index);
+ }
}
+ return super.getMember(mbr);
}
@Override
@@ -215,11 +400,21 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
if ((Channel.SND_TX_SEQ & svc) == Channel.SND_TX_SEQ) {
super.start(Channel.SND_TX_SEQ);
}
- final ChannelInterceptorBase base = this;
ScheduledExecutorService executor = getChannel().getUtilityExecutor();
- for (final Member member : members) {
+ Member[] configured;
+ synchronized (members) {
+ configured = members.toArray(new Member[0]);
+ }
+ for (final Member member : configured) {
Runnable r = () -> {
- base.memberAdded(member);
+ synchronized (members) {
+ if (!members.contains(member)) {
+ // replaced by the dynamically discovered member in
the meantime
+ return;
+ }
+ }
+ // the configured member is not a dynamically discovered member
+ StaticMembershipInterceptor.super.memberAdded(member);
if (getfirstInterceptor().getMember(member) != null) {
sendLocalMember(new Member[] { member });
}
@@ -229,13 +424,10 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
super.start(svc & (~Channel.SND_RX_SEQ) & (~Channel.SND_TX_SEQ));
// check required interceptors
- TcpFailureDetector failureDetector = null;
+ TcpFailureDetector failureDetector = getFailureDetector();
TcpPingInterceptor pingInterceptor = null;
ChannelInterceptor prev = getPrevious();
while (prev != null) {
- if (prev instanceof TcpFailureDetector) {
- failureDetector = (TcpFailureDetector) prev;
- }
if (prev instanceof TcpPingInterceptor) {
pingInterceptor = (TcpPingInterceptor) prev;
}
@@ -264,6 +456,7 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
/**
* Sends a local member start message to the given members.
+ *
* @param members the members to notify
*/
protected void sendLocalMember(Member[] members) {
@@ -276,6 +469,7 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
/**
* Sends a shutdown message to the given members.
+ *
* @param members the members to notify
*/
protected void sendShutdown(Member[] members) {
@@ -288,6 +482,7 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
/**
* Returns the first (bottom-most) interceptor in the chain.
+ *
* @return the first channel interceptor
*/
protected ChannelInterceptor getfirstInterceptor() {
@@ -302,8 +497,10 @@ public class StaticMembershipInterceptor extends
ChannelInterceptorBase implemen
/**
* Sends a member message to the given members.
+ *
* @param members the members to send to
* @param message the message payload
+ *
* @throws ChannelException if sending fails
*/
protected void sendMemberMessage(Member[] members, byte[] message) throws
ChannelException {
diff --git
a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
index 6a8a48c6a0..5eb8f5ffae 100644
--- a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
+++ b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
@@ -215,6 +215,34 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
}
}
+ /**
+ * Forgets a member that is represented by another member from now on, for
example a statically configured
+ * member whose node has been discovered dynamically. Unlike {@link
#memberDisappeared(Member)}, the member is
+ * not kept as a suspect, so it is not added again by the member checks.
If the member was a member of this
+ * detector, the interceptors above are told that it disappeared.
+ *
+ * @param member the member to forget
+ */
+ public void forgetMember(Member member) {
+ if (membership == null) {
+ setupMembership();
+ }
+ boolean removed;
+ synchronized (membership) {
+ // supersedes running checks of the member
+ recordMemberEvent(member);
+ removed = membership.contains(member);
+ if (removed) {
+ membership.removeMember(member);
+ }
+ removeSuspects.remove(member);
+ addSuspects.remove(member);
+ }
+ if (removed) {
+ super.memberDisappeared(member);
+ }
+ }
+
@Override
public void memberDisappeared(Member member) {
if (membership == null) {
diff --git
a/test/org/apache/catalina/tribes/group/interceptors/TestStaticMembershipInterceptor.java
b/test/org/apache/catalina/tribes/group/interceptors/TestStaticMembershipInterceptor.java
index 8d0ee184ec..0eb92c6cf4 100644
---
a/test/org/apache/catalina/tribes/group/interceptors/TestStaticMembershipInterceptor.java
+++
b/test/org/apache/catalina/tribes/group/interceptors/TestStaticMembershipInterceptor.java
@@ -16,12 +16,20 @@
*/
package org.apache.catalina.tribes.group.interceptors;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.tribes.Member;
import org.apache.catalina.tribes.group.ChannelInterceptorBase;
import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.membership.StaticMember;
public class TestStaticMembershipInterceptor {
@@ -34,181 +42,518 @@ public class TestStaticMembershipInterceptor {
public Member[] getMembers() {
return members;
}
+
+ @Override
+ public Member getLocalMember(boolean incAlive) {
+ return localMember;
+ }
+
+ private final Member localMember = createLocalMember();
+
+ private static Member createLocalMember() {
+ try {
+ return new StaticMember("localhost", 5000, -1);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
- private static MemberImpl member(String host, int port) throws Exception {
- return new MemberImpl(host, port, -1);
+ // Upstream stand-in: records the member events the interceptor forwards
+ private static class RecordingInterceptor extends ChannelInterceptorBase {
+
+ final List<Member> added = Collections.synchronizedList(new
ArrayList<>());
+ final List<Member> disappeared = Collections.synchronizedList(new
ArrayList<>());
+ // The events in the order they were received
+ final List<String> events = Collections.synchronizedList(new
ArrayList<>());
+
+ @Override
+ public void memberAdded(Member member) {
+ added.add(member);
+ events.add("added:" + member.getUniqueId()[0]);
+ }
+
+ @Override
+ public void memberDisappeared(Member member) {
+ disappeared.add(member);
+ events.add("disappeared:" + member.getUniqueId()[0]);
+ }
}
- /*
- * The copy the channel sees through dynamic discovery for the node
represented by the
- * given statically configured member: the same host and TCP port but a
different unique
- * id, since the unique id of a node that does not pin one explicitly is
random per JVM
- * start and cannot match the statically configured copy.
- */
- private static MemberImpl discoveredCopy(MemberImpl configured) throws
Exception {
- return member("localhost", configured.getPort());
+ // Failure detector that does not use the network
+ private static class AliveDetector extends TcpFailureDetector {
+
+ volatile boolean alive = true;
+
+ @Override
+ protected boolean memberAlive(Member mbr) {
+ return alive;
+ }
+ }
+
+ // Failure detector that holds the withdrawal of a replaced member
+ private static class GatedForgetDetector extends AliveDetector {
+
+ final CountDownLatch forgetStarted = new CountDownLatch(1);
+ final CountDownLatch releaseForget = new CountDownLatch(1);
+
+ @Override
+ public void forgetMember(Member member) {
+ forgetStarted.countDown();
+ try {
+ Assert.assertTrue("Timed out waiting to release the
withdrawal",
+ releaseForget.await(10, TimeUnit.SECONDS));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ super.forgetMember(member);
+ }
}
- private static void setUniqueId(MemberImpl member, int first) {
+ private final StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
+ private final DynamicMembershipChain chain = new DynamicMembershipChain();
+ private final RecordingInterceptor recorder = new RecordingInterceptor();
+
+ public TestStaticMembershipInterceptor() {
+ interceptor.setNext(chain);
+ interceptor.setPrevious(recorder);
+ }
+
+ private static StaticMember member(String host, int port, int
uniqueIdStart) throws Exception {
+ StaticMember member = new StaticMember(host, port, -1);
byte[] uniqueId = new byte[16];
for (int i = 0; i < uniqueId.length; i++) {
- uniqueId[i] = (byte) (first + i);
+ uniqueId[i] = (byte) (uniqueIdStart + i);
}
member.setUniqueId(uniqueId);
+ return member;
}
- private static int countOfPort(Member[] members, int port) {
- int count = 0;
- for (Member m : members) {
- int effectivePort = m.getPort() >= 0 ? m.getPort() :
m.getSecurePort();
- if (effectivePort == port) {
- count++;
- }
+ /*
+ * The member the channel sees through dynamic discovery for the node that
is configured statically as the
+ * given member: the same host and ports, but a different unique id, since
the unique id of a dynamically
+ * discovered node is random per JVM start and can not be known by the
static configuration.
+ */
+ private static MemberImpl discoveredCopy(MemberImpl configured, int
uniqueIdStart) throws Exception {
+ MemberImpl copy = new MemberImpl("localhost", configured.getPort(),
-1);
+ copy.setUniqueId(member("localhost", configured.getPort(),
uniqueIdStart).getUniqueId());
+ copy.setSecurePort(configured.getSecurePort());
+ copy.setUdpPort(configured.getUdpPort());
+ return copy;
+ }
+
+ // Puts a failure detector between the interceptor and the recorder, as in
the documented stack
+ private AliveDetector installDetector() {
+ return installDetector(new AliveDetector());
+ }
+
+ private AliveDetector installDetector(AliveDetector detector) {
+ detector.setNext(interceptor);
+ detector.setPrevious(recorder);
+ interceptor.setPrevious(detector);
+ return detector;
+ }
+
+ private void discover(Member... discovered) {
+ chain.members = discovered;
+ for (Member member : discovered) {
+ interceptor.memberAdded(member);
}
- return count;
}
@Test
- public void testGetMembersDeduplicatesStaticAndDynamic() throws Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testDiscoveredMemberReplacesConfiguredMember() throws
Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ Assert.assertNotEquals("The two copies must not be equal by
Member.equals", configured, discovered);
+ interceptor.addStaticMember(configured);
- MemberImpl staticOnly = member("localhost", 4000);
- setUniqueId(staticOnly, 1);
- MemberImpl bothConfigured = member("localhost", 4001);
- setUniqueId(bothConfigured, 17);
- MemberImpl dynamicOnly = member("localhost", 4002);
- setUniqueId(dynamicOnly, 33);
- // The statically configured node is also discovered dynamically, as a
- // different instance of the same node with a different unique id, so
- // Member.equals does not match the two copies
- MemberImpl bothDiscovered = discoveredCopy(bothConfigured);
- setUniqueId(bothDiscovered, 49);
- Assert.assertNotEquals("The two copies must not be equal by
Member.equals",
- bothConfigured, bothDiscovered);
- chain.members = new Member[] { dynamicOnly, bothDiscovered };
- interceptor.addStaticMember(bothConfigured);
- interceptor.addStaticMember(staticOnly);
+ discover(discovered);
Member[] result = interceptor.getMembers();
- Assert.assertEquals("Each destination must be returned exactly once",
3, result.length);
- Assert.assertEquals(1, countOfPort(result, 4000));
- Assert.assertEquals(1, countOfPort(result, 4001));
- Assert.assertEquals(1, countOfPort(result, 4002));
+ Assert.assertEquals("The node must be returned exactly once", 1,
result.length);
+ Assert.assertSame("The discovered member must replace the configured
member", discovered, result[0]);
+ Assert.assertTrue("The configured member must have been replaced",
interceptor.members.isEmpty());
+ Assert.assertEquals("The discovered member must be forwarded", 1,
recorder.added.size());
+ Assert.assertSame(discovered, recorder.added.get(0));
+ Assert.assertTrue("A configured member that was never announced must
not be withdrawn",
+ recorder.disappeared.isEmpty());
}
@Test
- public void testGetMembersDeduplicatesSecurePortOnlyListeners() throws
Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testReplacedConfiguredMemberIsForgottenByFailureDetector()
throws Exception {
+ AliveDetector detector = installDetector();
+ StaticMember configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ // announced as the interceptor does when it starts
+ detector.memberAdded(configured);
+ Assert.assertNotNull(detector.getMember(configured));
+
+ discover(discovered);
+
+ Member[] result = detector.getMembers();
+ Assert.assertEquals("The failure detector must know the node once", 1,
result.length);
+ Assert.assertSame(discovered, result[0]);
+ Assert.assertEquals("The configured member must be withdrawn before
the discovered member is announced",
+ Arrays.asList("added:17", "disappeared:17", "added:49"),
recorder.events);
+ }
+
+ @Test
+ public void
testReplacedConfiguredMemberIsNotAddedAgainByFailureDetectorHeartbeat() throws
Exception {
+ AliveDetector detector = installDetector();
+ StaticMember configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ detector.memberAdded(configured);
+ discover(discovered);
+
+ detector.checkMembers(false);
+ detector.checkMembers(true);
+
+ Member[] result = detector.getMembers();
+ Assert.assertEquals("The configured member must not be added again",
1, result.length);
+ Assert.assertSame(discovered, result[0]);
+ Assert.assertEquals(Arrays.asList("added:17", "disappeared:17",
"added:49"), recorder.events);
+ }
+
+ @Test
+ public void
testReplacedConfiguredMemberRejectedByFailureDetectorIsNotAddedLater() throws
Exception {
+ AliveDetector detector = installDetector();
+ StaticMember configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ // the node is not reachable yet, so the failure detector keeps the
configured member as a suspect
+ detector.alive = false;
+ detector.memberAdded(configured);
+ Assert.assertNull(detector.getMember(configured));
+ Assert.assertTrue(detector.addSuspects.containsKey(configured));
+
+ detector.alive = true;
+ discover(discovered);
+ detector.checkMembers(false);
+
+ Member[] result = detector.getMembers();
+ Assert.assertEquals("The configured member must not be added later",
1, result.length);
+ Assert.assertSame(discovered, result[0]);
+ Assert.assertTrue(detector.addSuspects.isEmpty());
+ Assert.assertEquals("Nothing was announced for the configured member",
Arrays.asList("added:49"),
+ recorder.events);
+ }
+
+ @Test
+ public void testDisappearanceDuringReplacementLeavesOnlyConfiguredMember()
throws Exception {
+ GatedForgetDetector detector = new GatedForgetDetector();
+ installDetector(detector);
+ StaticMember configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ detector.memberAdded(configured);
+ chain.members = new Member[] { discovered };
+
+ // The replacement is held after the configured member was replaced,
before the discovered member is
+ // announced
+ Thread replacementThread = new Thread(() ->
interceptor.memberAdded(discovered));
+ replacementThread.start();
+ Assert.assertTrue("The withdrawal did not start",
detector.forgetStarted.await(10, TimeUnit.SECONDS));
+ Assert.assertTrue(interceptor.members.isEmpty());
+
+ // The discovered member disappears meanwhile, without waiting for the
replacement
+ chain.members = new Member[0];
+ interceptor.memberDisappeared(discovered);
+ Assert.assertSame("The configured member is restored", configured,
interceptor.members.get(0));
+
+ detector.releaseForget.countDown();
+ replacementThread.join(10000);
+ Assert.assertFalse(replacementThread.isAlive());
+
+ // The discovered member that was announced after it had disappeared
must not be kept
+ Assert.assertNull("The announcement of a member that is gone must be
undone", detector.getMember(discovered));
+ detector.checkMembers(false);
+ Member[] result = detector.getMembers();
+ Assert.assertEquals("Only the configured member remains", 1,
result.length);
+ Assert.assertSame(configured, result[0]);
+ }
+
+ @Test
+ public void
testDisappearanceDuringAddingStaticMemberLeavesOnlyConfiguredMember() throws
Exception {
+ GatedForgetDetector detector = new GatedForgetDetector();
+ installDetector(detector);
+ StaticMember configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ detector.memberAdded(configured);
+ chain.members = new Member[] { discovered };
+ detector.memberAdded(discovered);
+
+ Thread addThread = new Thread(() ->
interceptor.addStaticMember(configured));
+ addThread.start();
+ Assert.assertTrue("The withdrawal did not start",
detector.forgetStarted.await(10, TimeUnit.SECONDS));
+ Assert.assertTrue(interceptor.members.isEmpty());
+
+ // The discovered member disappears meanwhile, and is not alive
+ chain.members = new Member[0];
+ detector.alive = false;
+ interceptor.memberDisappeared(discovered);
+ Assert.assertSame("The configured member is restored", configured,
interceptor.members.get(0));
+
+ detector.alive = true;
+ detector.releaseForget.countDown();
+ addThread.join(10000);
+ Assert.assertFalse(addThread.isAlive());
+
+ // The withdrawal was applied to the restored member, the next
heartbeat finds it again
+ detector.checkMembers(false);
+ Member[] result = detector.getMembers();
+ Assert.assertEquals("Only the configured member remains", 1,
result.length);
+ Assert.assertSame(configured, result[0]);
+ }
+
+ @Test
+ public void testDiscoveredMemberWithSameUniqueIdReplacesConfiguredMember()
throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 17);
+ Assert.assertEquals(configured, discovered);
+ interceptor.addStaticMember(configured);
- MemberImpl bothConfigured = new MemberImpl("localhost", -1, -1);
- bothConfigured.setSecurePort(4433);
- setUniqueId(bothConfigured, 17);
- MemberImpl bothDiscovered = new MemberImpl("localhost", -1, -1);
- bothDiscovered.setSecurePort(4433);
- setUniqueId(bothDiscovered, 49);
- chain.members = new Member[] { bothDiscovered };
- interceptor.addStaticMember(bothConfigured);
+ discover(discovered);
Member[] result = interceptor.getMembers();
- Assert.assertEquals("The secure port destination must be returned
once", 1, result.length);
+ Assert.assertEquals("The node must be returned exactly once", 1,
result.length);
+ Assert.assertSame(discovered, result[0]);
}
@Test
- public void testGetMembersKeepsDifferentDestinations() throws Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testSecureAndUdpPortsMustAllMatch() throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ configured.setSecurePort(4433);
+ configured.setUdpPort(4500);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
- // Same host, different port: distinct nodes, both must be kept
- MemberImpl dynamic = member("localhost", 4000);
- setUniqueId(dynamic, 1);
- MemberImpl staticOtherPort = member("localhost", 4001);
- setUniqueId(staticOtherPort, 17);
- chain.members = new Member[] { dynamic };
- interceptor.addStaticMember(staticOtherPort);
+ discover(discovered);
Member[] result = interceptor.getMembers();
- Assert.assertEquals(2, result.length);
- Assert.assertEquals(1, countOfPort(result, 4000));
- Assert.assertEquals(1, countOfPort(result, 4001));
+ Assert.assertEquals(1, result.length);
+ Assert.assertSame(discovered, result[0]);
}
@Test
- public void testGetMembersKeepsSamePortOnDifferentHosts() throws Exception
{
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testSecurePortOnlyMismatchIsNotReplaced() throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ discovered.setSecurePort(4433);
+ interceptor.addStaticMember(configured);
+
+ discover(discovered);
+
+ Assert.assertEquals("A partial port match is a configuration error and
is not merged", 2,
+ interceptor.getMembers().length);
+ Assert.assertTrue(interceptor.members.contains(configured));
+ }
+
+ @Test
+ public void testUdpPortOnlyMismatchIsNotReplaced() throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ discovered.setUdpPort(4500);
+ interceptor.addStaticMember(configured);
+
+ discover(discovered);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ Assert.assertTrue(interceptor.members.contains(configured));
+ }
- MemberImpl dynamic = member("127.0.0.1", 4000);
- setUniqueId(dynamic, 1);
- MemberImpl staticOtherHost = member("127.0.0.2", 4000);
- setUniqueId(staticOtherHost, 17);
- chain.members = new Member[] { dynamic };
- interceptor.addStaticMember(staticOtherHost);
+ @Test
+ public void testTcpPortOnlyMismatchIsNotReplaced() throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ configured.setSecurePort(4433);
+ MemberImpl discovered = member("localhost", 4002, 49);
+ discovered.setSecurePort(4433);
+ interceptor.addStaticMember(configured);
+
+ discover(discovered);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ Assert.assertTrue(interceptor.members.contains(configured));
+ }
+
+ @Test
+ public void testDifferentTcpPortOnSameHostIsDifferentNode() throws
Exception {
+ // Two nodes on one host that only differ in the TCP port, secure and
UDP ports unused
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = member("localhost", 4000, 1);
+ interceptor.addStaticMember(configured);
+
+ discover(discovered);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ Assert.assertTrue(interceptor.members.contains(configured));
+ }
+
+ @Test
+ public void testSamePortOnDifferentHostsIsDifferentNode() throws Exception
{
+ MemberImpl configured = member("127.0.0.2", 4000, 17);
+ MemberImpl discovered = member("127.0.0.1", 4000, 1);
+ interceptor.addStaticMember(configured);
+
+ discover(discovered);
+
+ Assert.assertEquals("Two hosts may share a port", 2,
interceptor.getMembers().length);
+ Assert.assertTrue(interceptor.members.contains(configured));
+ }
+
+ @Test
+ public void testConfiguredMemberRestoredWhenDiscoveredMemberDisappears()
throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ discover(discovered);
+
+ chain.members = new Member[0];
+ interceptor.memberDisappeared(discovered);
Member[] result = interceptor.getMembers();
- Assert.assertEquals("Two hosts may share a port", 2, result.length);
+ Assert.assertEquals("The configured node stays a member", 1,
result.length);
+ Assert.assertSame(configured, result[0]);
+ Assert.assertEquals("The disappearance must be forwarded", 1,
recorder.disappeared.size());
+ Assert.assertSame(discovered, recorder.disappeared.get(0));
}
@Test
- public void testGetMembersAllStatic() throws Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testRestartedNodeReplacesConfiguredMemberAgain() throws
Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl before = discoveredCopy(configured, 49);
+ MemberImpl after = discoveredCopy(configured, 65);
+ interceptor.addStaticMember(configured);
+ discover(before);
+
+ // The restarted node announces a new unique id before the old one is
dropped
+ chain.members = new Member[] { before, after };
+ interceptor.memberAdded(after);
+ chain.members = new Member[] { after };
+ interceptor.memberDisappeared(before);
+
+ Member[] result = interceptor.getMembers();
+ Assert.assertEquals("The node must be returned exactly once", 1,
result.length);
+ Assert.assertSame(after, result[0]);
+ Assert.assertTrue("The configured member must not be restored",
interceptor.members.isEmpty());
+ }
- MemberImpl first = member("localhost", 4000);
- setUniqueId(first, 1);
- MemberImpl second = member("localhost", 4001);
- setUniqueId(second, 17);
+ @Test
+ public void testConfiguredMemberAddedAfterDiscoveryIsReplaced() throws
Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ chain.members = new Member[] { discovered };
+
+ interceptor.addStaticMember(configured);
+
+ Member[] result = interceptor.getMembers();
+ Assert.assertEquals(1, result.length);
+ Assert.assertSame(discovered, result[0]);
+ }
+
+ @Test
+ public void testRemovedConfiguredMemberIsNotRestored() throws Exception {
+ MemberImpl configured = member("localhost", 4001, 17);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ discover(discovered);
+
+ interceptor.removeStaticMember(configured);
+ chain.members = new Member[0];
+ interceptor.memberDisappeared(discovered);
+
+ Assert.assertEquals(0, interceptor.getMembers().length);
+ }
+
+ @Test
+ public void testGetMembersAllStatic() throws Exception {
+ MemberImpl first = member("localhost", 4000, 1);
+ MemberImpl second = member("localhost", 4001, 17);
interceptor.addStaticMember(first);
interceptor.addStaticMember(second);
interceptor.addStaticMember(first);
- Member[] result = interceptor.getMembers();
- Assert.assertEquals("Each static destination must be returned exactly
once", 2, result.length);
- Assert.assertEquals(1, countOfPort(result, 4000));
- Assert.assertEquals(1, countOfPort(result, 4001));
+ Assert.assertEquals("Each static member must be returned exactly
once", 2, interceptor.getMembers().length);
}
@Test
public void testGetMembersCollidesSameDestinationTwiceStatic() throws
Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
-
- // The same node configured twice with different unique ids: a
- // destination misconfiguration, collapsed to one entry
- MemberImpl first = member("localhost", 4000);
- setUniqueId(first, 1);
- MemberImpl second = member("localhost", 4000);
- setUniqueId(second, 17);
+ // The same node configured twice with different unique ids is a
configuration error, the later entry is
+ // ignored
+ StaticMember first = member("localhost", 4000, 1);
+ StaticMember second = member("localhost", 4000, 17);
interceptor.addStaticMember(first);
interceptor.addStaticMember(second);
Member[] result = interceptor.getMembers();
Assert.assertEquals("One destination must be returned once", 1,
result.length);
+ Assert.assertSame("The first entry is kept", first, result[0]);
}
@Test
- public void testGetMembersAllDynamic() throws Exception {
- StaticMembershipInterceptor interceptor = new
StaticMembershipInterceptor();
- DynamicMembershipChain chain = new DynamicMembershipChain();
- interceptor.setNext(chain);
+ public void testPartiallyMatchingStaticMembersAreKept() throws Exception {
+ // Reported as a configuration error, but not merged
+ StaticMember first = member("localhost", 4000, 1);
+ StaticMember second = member("localhost", 4000, 17);
+ second.setSecurePort(4433);
+ interceptor.addStaticMember(first);
+ interceptor.addStaticMember(second);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ }
- MemberImpl first = member("localhost", 4000);
- setUniqueId(first, 1);
- MemberImpl second = member("localhost", 4001);
- setUniqueId(second, 17);
- chain.members = new Member[] { first, second };
+ @Test
+ public void testStaticMembersOnSameHostWithDifferentTcpPortsAreKept()
throws Exception {
+ interceptor.addStaticMember(member("localhost", 4000, 1));
+ interceptor.addStaticMember(member("localhost", 4001, 17));
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ }
+
+ @Test
+ public void testDuplicateOfReplacedStaticMemberIsIgnored() throws
Exception {
+ StaticMember configured = member("localhost", 4001, 17);
+ StaticMember duplicate = member("localhost", 4001, 33);
+ MemberImpl discovered = discoveredCopy(configured, 49);
+ interceptor.addStaticMember(configured);
+ discover(discovered);
+
+ // The first entry is currently replaced by the discovered member, the
duplicate must not come back
+ interceptor.addStaticMember(duplicate);
Member[] result = interceptor.getMembers();
- Assert.assertEquals(2, result.length);
- Assert.assertEquals(1, countOfPort(result, 4000));
- Assert.assertEquals(1, countOfPort(result, 4001));
+ Assert.assertEquals(1, result.length);
+ Assert.assertSame(discovered, result[0]);
+ Assert.assertTrue(interceptor.members.isEmpty());
+
+ chain.members = new Member[0];
+ interceptor.memberDisappeared(discovered);
+ result = interceptor.getMembers();
+ Assert.assertEquals(1, result.length);
+ Assert.assertSame("The first entry is restored", configured,
result[0]);
+ }
+
+ @Test
+ public void testGetMembersAllDynamic() throws Exception {
+ MemberImpl first = member("localhost", 4000, 1);
+ MemberImpl second = member("localhost", 4001, 17);
+ discover(first, second);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
+ Assert.assertEquals(2, recorder.added.size());
+ }
+
+ @Test
+ public void testGetMembersMergesStaticAndDynamic() throws Exception {
+ MemberImpl staticOnly = member("localhost", 4000, 1);
+ MemberImpl dynamicOnly = member("localhost", 4002, 33);
+ interceptor.addStaticMember(staticOnly);
+
+ discover(dynamicOnly);
+
+ Assert.assertEquals(2, interceptor.getMembers().length);
}
}
diff --git
a/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
b/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
index 03ca67b2e9..e09322367d 100644
---
a/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
+++
b/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
@@ -505,4 +505,69 @@ public class TestTcpFailureDetectorLivenessChecks {
detector.getMember(member));
Assert.assertEquals("Only the forced check may have reported the
member", 1, recorder.added.size());
}
+
+ @Test
+ public void testForgetMemberIsNotAddedAgainByHeartbeat() throws Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ CheckRecordingDetector detector = createDetector(chain, recorder);
+ StaticMember member = new StaticMember("localhost", 4000, -1);
+ detector.setAlive(true);
+ detector.memberAdded(member);
+ Assert.assertNotNull(detector.getMember(member));
+
+ detector.forgetMember(member);
+
+ Assert.assertNull("The member must be removed",
detector.getMember(member));
+ Assert.assertEquals("The disappearance must be forwarded once", 1,
recorder.disappeared.size());
+ Assert.assertTrue("The member must not become a suspect",
detector.addSuspects.isEmpty());
+ Assert.assertTrue(detector.removeSuspects.isEmpty());
+ detector.checkMembers(false);
+ Assert.assertNull("The heartbeat must not add the member again",
detector.getMember(member));
+ Assert.assertEquals(1, recorder.added.size());
+ }
+
+ @Test
+ public void testForgetMemberClearsSuspectThatWasNeverAdded() throws
Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ CheckRecordingDetector detector = createDetector(chain, recorder);
+ StaticMember member = new StaticMember("localhost", 4000, -1);
+ detector.setAlive(false);
+ detector.memberAdded(member);
+ Assert.assertTrue(detector.addSuspects.containsKey(member));
+
+ detector.forgetMember(member);
+
+ Assert.assertTrue("The suspect must be forgotten",
detector.addSuspects.isEmpty());
+ Assert.assertTrue("A member the detector never accepted is not
reported", recorder.disappeared.isEmpty());
+ detector.setAlive(true);
+ detector.checkMembers(false);
+ Assert.assertNull("The heartbeat must not add the member",
detector.getMember(member));
+ Assert.assertTrue(recorder.added.isEmpty());
+ }
+
+ @Test
+ public void testForgetMemberSupersedesRunningAddCheck() throws Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ GatedAddDetector detector = new GatedAddDetector();
+ detector.setChannel(new GroupChannel());
+ detector.setNext(chain);
+ detector.setPrevious(recorder);
+ chain.localMember = new StaticMember("localhost", 5000, -1);
+ StaticMember member = new StaticMember("localhost", GATED_PORT, -1);
+ Thread addThread = new Thread(() -> detector.memberAdded(member));
+ addThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+
+ detector.forgetMember(member);
+ detector.releaseCheck.countDown();
+ addThread.join(10000);
+
+ Assert.assertFalse("Member add thread did not finish",
addThread.isAlive());
+ Assert.assertNull("A forgotten member must not be added by a running
check", detector.getMember(member));
+ Assert.assertTrue(recorder.added.isEmpty());
+ }
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 0d4e0e7947..cf02d5676d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -588,6 +588,26 @@
with membership listeners that call back into the
interceptor. (remm)
</fix>
+ <fix>
+ When a node that is configured with the
+ <code>StaticMembershipInterceptor</code> is also discovered
+ dynamically, the <code>StaticMembershipInterceptor</code> now
+ replaces the statically configured member with the dynamically
+ discovered member if the host and all of the ports (TCP, secure
+ and UDP) match. Previously, the node was a member twice because
+ the unique id of a dynamically discovered member is random and
+ can not match the statically configured unique id, and every
+ message sent to all members was sent to the node twice. If only
+ some of the ports match, the mismatch is logged as a
+ configuration error and the members are not merged. The
+ failure detector is told to forget the statically configured
+ member before the discovered member is announced, so that it is
+ not added again by the member checks. If the dynamically
+ discovered member disappears, the statically configured member
+ is restored. A second statically configured member with the
+ same host and ports as an earlier one is logged as a
+ configuration error and ignored. (markt)
+ </fix>
<fix>
The <code>TwoPhaseCommitInterceptor</code> now forwards the
interceptor payload, and with it the <code>ErrorHandler</code>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]