This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new 7a8335428a Review "Run TcpFailureDetector liveness checks outside the
... monitor"
7a8335428a is described below
commit 7a8335428a9ad7aa63fc0341cf988614b044d95b
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Oct 21 17:46:05 2026 +0100
Review "Run TcpFailureDetector liveness checks outside the ... monitor"
Correctly handle events received during the liveness probe
Co-authored by: Claude / Sonnet 5.5
Co-authored by: CoPilot / GPT-5.6 Sol
---
.../group/interceptors/TcpFailureDetector.java | 240 ++++++++++++----
.../TestTcpFailureDetectorLivenessChecks.java | 304 +++++++++++++++++++++
2 files changed, 487 insertions(+), 57 deletions(-)
diff --git
a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
index 161b482539..6a8a48c6a0 100644
--- a/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
+++ b/java/org/apache/catalina/tribes/group/interceptors/TcpFailureDetector.java
@@ -22,9 +22,11 @@ import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.net.SocketTimeoutException;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
import org.apache.catalina.tribes.Channel;
import org.apache.catalina.tribes.ChannelException;
@@ -102,6 +104,24 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
*/
protected Membership membership = null;
+ /**
+ * The sequence number of the last membership event of each member, used
to detect that a member event
+ * was processed while a liveness check of the same member was running.
Only populated while at least
+ * one liveness check is running. Guarded by {@link #membership}.
+ */
+ private final HashMap<Member,Long> lastMemberEvent = new HashMap<>();
+
+ /**
+ * The last membership event sequence number issued. Guarded by {@link
#membership}.
+ */
+ private long memberEventSequence = 0;
+
+ /**
+ * The number of liveness checks that have started and not yet been
applied. Guarded by
+ * {@link #membership}.
+ */
+ private int checksInFlight = 0;
+
/**
* Map of members suspected of being dead and their detection time.
*/
@@ -160,24 +180,28 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
}
boolean notify = false;
boolean verify = false;
+ long stamp = 0;
synchronized (membership) {
+ recordMemberEvent(member);
if (removeSuspects.containsKey(member)) {
// previously marked suspect, system below picked up the
member again
removeSuspects.remove(member);
} else if (membership.getMember(member) == null) {
verify = true;
+ stamp = beginCheck(member);
}
}
if (verify) {
// if we add it here, then add it upwards too
// check to see if it is alive, outside the membership monitor so
// that slow TCP connects do not block other membership operations
- boolean alive = memberAlive(member);
+ boolean alive = memberAliveForCheck(member, stamp);
synchronized (membership) {
// the state may have changed while the member was being
checked
- if (!removeSuspects.containsKey(member) &&
membership.getMember(member) == null) {
+ if (endCheck(member, stamp) && membership.getMember(member) ==
null) {
if (alive) {
membership.memberAlive(member);
+ recordMemberEvent(member);
addSuspects.remove(member);
notify = true;
} else if (member instanceof StaticMember) {
@@ -197,8 +221,10 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
setupMembership();
}
boolean shutdown = Arrays.equals(member.getCommand(),
Member.SHUTDOWN_PAYLOAD);
+ long stamp = 0;
if (shutdown) {
synchronized (membership) {
+ recordMemberEvent(member);
if (!membership.contains(member)) {
return;
}
@@ -212,7 +238,11 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
} else {
boolean verify;
synchronized (membership) {
+ recordMemberEvent(member);
verify = membership.contains(member);
+ if (verify) {
+ stamp = beginCheck(member);
+ }
}
if (!verify) {
if (log.isInfoEnabled()) {
@@ -228,17 +258,19 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
// check to see if the member really is gone, outside the
membership
// monitor so that slow TCP connects do not block other membership
// operations
- boolean alive = memberAlive(member);
+ boolean alive = memberAliveForCheck(member, stamp);
synchronized (membership) {
// the state may have changed while the member was being
checked
- if (membership.contains(member)) {
+ if (endCheck(member, stamp) && membership.contains(member)) {
applied = true;
if (alive) {
// add the member as suspect
removeSuspects.put(member,
Long.valueOf(System.currentTimeMillis()));
+ recordMemberEvent(member);
} else {
// not correct, we need to maintain the map
membership.removeMember(member);
+ recordMemberEvent(member);
removeSuspects.remove(member);
if (member instanceof StaticMember) {
addSuspects.put(member,
Long.valueOf(System.currentTimeMillis()));
@@ -321,18 +353,28 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
Member[] members = super.getMembers();
for (int i = 0; members != null && i < members.length; i++) {
Member member = members[i];
- boolean alive = memberAlive(member);
+ long stamp;
+ synchronized (membership) {
+ stamp = beginCheck(member);
+ }
+ boolean alive = memberAliveForCheck(member, stamp);
boolean added = false;
boolean disappeared = false;
synchronized (membership) {
+ if (!endCheck(member, stamp)) {
+ // a member event for this member was processed meanwhile
and wins
+ continue;
+ }
if (alive) {
if (membership.memberAlive(member)) {
added = true;
+ recordMemberEvent(member);
}
addSuspects.remove(member);
} else {
if (membership.getMember(member) != null) {
membership.removeMember(member);
+ recordMemberEvent(member);
removeSuspects.remove(member);
if (member instanceof StaticMember) {
addSuspects.put(member,
Long.valueOf(System.currentTimeMillis()));
@@ -358,38 +400,53 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
protected void performBasicCheck() {
// update all alive times
Member[] members = super.getMembers();
- ArrayList<Member> toCheck = new ArrayList<>();
- for (int i = 0; members != null && i < members.length; i++) {
- synchronized (membership) {
- if (addSuspects.containsKey(members[i]) &&
membership.getMember(members[i]) == null) {
- // avoid temporary adding member.
- continue;
+ // Members unknown to the tracker and the stamps of their running
checks. They are only added to the
+ // tracker once the check has found them alive, so an unverified
member is never visible and a stale
+ // result can simply be discarded.
+ Map<Member,Long> toCheck = new LinkedHashMap<>();
+ try {
+ for (int i = 0; members != null && i < members.length; i++) {
+ synchronized (membership) {
+ if (membership.getMember(members[i]) != null) {
+ // known member, just update its alive time
+ membership.memberAlive(members[i]);
+ } else if (!addSuspects.containsKey(members[i])) {
+ // we don't have this one in our membership, check to
see if the member is alive
+ // (members in addSuspects are not added temporarily,
see below)
+ toCheck.put(members[i],
Long.valueOf(beginCheck(members[i])));
+ }
}
- if (membership.memberAlive(members[i])) {
- // we don't have this one in our membership, check to see
if the member is alive
- toCheck.add(members[i]);
- } // end if
- }
- } // for
+ } // for
- // The liveness checks run outside the monitor, so a concurrent
memberDisappeared can
- // remove a member from the tracker between the two passes. The apply
below then skips
- // that member and the concurrent disappearance is the one that is
reported.
- for (Member member : toCheck) {
- boolean alive = memberAlive(member);
- boolean notify = false;
- synchronized (membership) {
- if (membership.getMember(member) != null) {
- if (alive) {
+ // The liveness checks run outside the monitor. A member event or
a change made by another check
+ // for the same member while it runs supersedes the result, which
is then discarded.
+ for (Iterator<Map.Entry<Member,Long>> it =
toCheck.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<Member,Long> entry = it.next();
+ it.remove();
+ Member member = entry.getKey();
+ long stamp = entry.getValue().longValue();
+ boolean alive = memberAliveForCheck(member, stamp);
+ boolean notify = false;
+ synchronized (membership) {
+ if (endCheck(member, stamp) && alive &&
membership.getMember(member) == null) {
+ membership.memberAlive(member);
+ recordMemberEvent(member);
notify = true;
- } else {
- membership.removeMember(member);
- } // end if
+ }
+ }
+ if (notify) {
+
log.warn(sm.getString("tcpFailureDetector.performBasicCheck.memberAdded",
member));
+ super.memberAdded(member);
}
}
- if (notify) {
-
log.warn(sm.getString("tcpFailureDetector.performBasicCheck.memberAdded",
member));
- super.memberAdded(member);
+ } finally {
+ if (!toCheck.isEmpty()) {
+ // an exception interrupted the checks, release the ones that
did not run
+ synchronized (membership) {
+ for (Map.Entry<Member,Long> entry : toCheck.entrySet()) {
+ endCheck(entry.getKey(), entry.getValue().longValue());
+ }
+ }
}
}
@@ -401,27 +458,29 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
}
for (Member m : keys) {
boolean verify;
+ long stamp = 0;
synchronized (membership) {
verify = membership.getMember(m) != null;
+ if (verify) {
+ stamp = beginCheck(m);
+ }
}
boolean disappeared = false;
if (verify) {
- boolean alive = memberAlive(m);
- if (!alive) {
- synchronized (membership) {
- // the state may have changed while the member was
being checked
- if (membership.getMember(m) != null) {
- membership.removeMember(m);
- if (m instanceof StaticMember) {
- addSuspects.put(m,
Long.valueOf(System.currentTimeMillis()));
- }
- removeSuspects.remove(m);
- disappeared = true;
- }
- }
- } else {
- synchronized (membership) {
+ boolean alive = memberAliveForCheck(m, stamp);
+ synchronized (membership) {
+ // the state may have changed while the member was being
checked
+ boolean current = endCheck(m, stamp);
+ if (current && alive) {
expireRemoveSuspect(m);
+ } else if (current && membership.getMember(m) != null) {
+ membership.removeMember(m);
+ recordMemberEvent(m);
+ if (m instanceof StaticMember) {
+ addSuspects.put(m,
Long.valueOf(System.currentTimeMillis()));
+ }
+ removeSuspects.remove(m);
+ disappeared = true;
}
}
} else {
@@ -444,22 +503,25 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
}
for (Member m : keys) {
boolean verify;
+ long stamp = 0;
synchronized (membership) {
verify = membership.getMember(m) == null;
+ if (verify) {
+ stamp = beginCheck(m);
+ }
}
if (!verify) {
continue;
}
- boolean alive = memberAlive(m);
+ boolean alive = memberAliveForCheck(m, stamp);
boolean notify = false;
- if (alive) {
- synchronized (membership) {
- // the state may have changed while the member was being
checked
- if (membership.getMember(m) == null) {
- membership.memberAlive(m);
- addSuspects.remove(m);
- notify = true;
- }
+ synchronized (membership) {
+ // the state may have changed while the member was being
checked
+ if (endCheck(m, stamp) && alive && membership.getMember(m) ==
null) {
+ membership.memberAlive(m);
+ recordMemberEvent(m);
+ addSuspects.remove(m);
+ notify = true;
}
}
if (notify) {
@@ -471,6 +533,70 @@ public class TcpFailureDetector extends
ChannelInterceptorBase implements TcpFai
}
}
+ /**
+ * Records that a member event was processed or a change was made to the
membership state of the member. A
+ * liveness check of the same member that started earlier is then stale
and must not be applied, since the
+ * later event or change wins. Events of other members do not affect it.
Must be called while holding the
+ * membership monitor.
+ *
+ * @param member the member the event is for
+ */
+ private void recordMemberEvent(Member member) {
+ if (checksInFlight > 0) {
+ lastMemberEvent.put(member, Long.valueOf(++memberEventSequence));
+ }
+ }
+
+ /**
+ * Registers the start of a liveness check of the member. Must be called
while holding the membership
+ * monitor and followed by a call to {@link #endCheck(Member, long)} when
the result is applied.
+ *
+ * @param member the member to be checked
+ * @return the stamp to pass to {@link #endCheck(Member, long)}
+ */
+ private long beginCheck(Member member) {
+ checksInFlight++;
+ Long last = lastMemberEvent.get(member);
+ return last == null ? 0 : last.longValue();
+ }
+
+ /**
+ * Registers the end of a liveness check of the member and reports if its
result is still current. Must
+ * be called while holding the membership monitor.
+ *
+ * @param member the member that was checked
+ * @param stamp the stamp returned by {@link #beginCheck(Member)}
+ * @return {@code true} if no member event for the member was processed
since the check started
+ */
+ private boolean endCheck(Member member, long stamp) {
+ Long last = lastMemberEvent.get(member);
+ boolean current = (last == null ? 0 : last.longValue()) == stamp;
+ if (--checksInFlight == 0) {
+ // no running check can compare against the recorded events any
more
+ lastMemberEvent.clear();
+ }
+ return current;
+ }
+
+ /**
+ * Performs a registered liveness check and releases its registration if
the check fails before its result can be
+ * applied.
+ *
+ * @param member the member to check
+ * @param stamp the stamp returned by {@link #beginCheck(Member)}
+ * @return {@code true} if the member is alive
+ */
+ private boolean memberAliveForCheck(Member member, long stamp) {
+ try {
+ return memberAlive(member);
+ } catch (RuntimeException | Error e) {
+ synchronized (membership) {
+ endCheck(member, stamp);
+ }
+ throw e;
+ }
+ }
+
/**
* Removes the member from the remove suspects map if it has been suspect
for longer than the
* configured timeout. Must be called while holding the membership monitor.
diff --git
a/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
b/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
index 676435c472..03ca67b2e9 100644
---
a/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
+++
b/test/org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetectorLivenessChecks.java
@@ -19,6 +19,10 @@ package org.apache.catalina.tribes.group.interceptors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import org.junit.Assert;
import org.junit.Test;
@@ -71,6 +75,45 @@ public class TestTcpFailureDetectorLivenessChecks {
}
}
+ private static final int GATED_PORT = 4000;
+
+ private static class GatedAddDetector extends TcpFailureDetector {
+
+ private final CountDownLatch checkStarted = new CountDownLatch(1);
+ private final CountDownLatch releaseCheck = new CountDownLatch(1);
+ private volatile boolean gatedResult = true;
+ // Result of the gated thread's checks after the first one, which was
held
+ private volatile boolean laterGatedResult = true;
+ private final AtomicInteger gatedChecks = new AtomicInteger();
+ // When set, only checks run by this thread are gated and all others
return otherResult
+ private volatile Thread gatedThread;
+ private volatile boolean otherResult = true;
+
+ @Override
+ protected boolean memberAlive(Member mbr) {
+ if (gatedThread != null) {
+ if (Thread.currentThread() != gatedThread) {
+ return otherResult;
+ }
+ } else if (mbr.getPort() != GATED_PORT) {
+ return true;
+ }
+ if (gatedChecks.incrementAndGet() > 1 && gatedThread != null) {
+ return laterGatedResult;
+ }
+ checkStarted.countDown();
+ try {
+ if (!releaseCheck.await(10, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timed out waiting to
release liveness check");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ }
+ return gatedResult;
+ }
+ }
+
// Downstream stand-in: provides the channel membership view the detector
checks
private static class FakeMembershipChain extends ChannelInterceptorBase {
@@ -201,4 +244,265 @@ public class TestTcpFailureDetectorLivenessChecks {
detector.assertNoCheckRanUnderMonitor();
}
+
+ @Test
+ public void testShutdownInvalidatesInFlightAddCheck() 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", 4000, -1);
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ Thread addThread = new Thread(() -> {
+ try {
+ detector.memberAdded(member);
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ });
+ addThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+
+ member.setCommand(Member.SHUTDOWN_PAYLOAD);
+ detector.memberDisappeared(member);
+ detector.releaseCheck.countDown();
+ addThread.join(10000);
+
+ Assert.assertFalse("Member add thread did not finish",
addThread.isAlive());
+ Assert.assertNull("Member add failed unexpectedly", failure.get());
+ Assert.assertNull("A stale successful check must not resurrect a
member after shutdown",
+ detector.getMember(member));
+ Assert.assertTrue("A stale add must not be forwarded",
recorder.added.isEmpty());
+ }
+
+ @Test
+ public void testEventOfOtherMemberDoesNotInvalidateAddCheck() 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 gated = new StaticMember("localhost", GATED_PORT, -1);
+ StaticMember other = new StaticMember("localhost", 4001, -1);
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ Thread addThread = new Thread(() -> {
+ try {
+ detector.memberAdded(gated);
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ });
+ addThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+
+ // Events of other members, including a disappearance, are processed
meanwhile
+ detector.memberAdded(other);
+ detector.memberDisappeared(other);
+ detector.releaseCheck.countDown();
+ addThread.join(10000);
+
+ Assert.assertFalse("Member add thread did not finish",
addThread.isAlive());
+ Assert.assertNull("Member add failed unexpectedly", failure.get());
+ Assert.assertNotNull("An event of another member must not drop the
add", detector.getMember(gated));
+ Assert.assertTrue("The add must be forwarded",
recorder.added.contains(gated));
+ }
+
+ @Test
+ public void testLaterEventOfSameMemberInvalidatesAddCheck() 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));
+
+ // A non-shutdown disappearance of a member not yet in the table is
ignored but still
+ // supersedes the running add check
+ detector.memberDisappeared(member);
+ detector.releaseCheck.countDown();
+ addThread.join(10000);
+
+ Assert.assertFalse("Member add thread did not finish",
addThread.isAlive());
+ Assert.assertNull("A later event for the member supersedes the running
check",
+ detector.getMember(member));
+ Assert.assertTrue("A superseded add must not be forwarded",
recorder.added.isEmpty());
+ }
+
+ /*
+ * Runs a basic check whose probe of the member is held, while the same
member is reported by the
+ * membership service, which runs its own (ungated) check with the given
result. Then the held probe is
+ * released with its own result.
+ */
+ private void runBasicCheckWithConcurrentAdd(boolean probeAlive, boolean
eventAlive, GatedAddDetector detector,
+ RecordingInterceptor recorder, FakeMembershipChain chain,
StaticMember member) throws Exception {
+ detector.setChannel(new GroupChannel());
+ detector.setNext(chain);
+ detector.setPrevious(recorder);
+ chain.localMember = new StaticMember("localhost", 5000, -1);
+ chain.members = new Member[] { member };
+ detector.gatedResult = probeAlive;
+ // the same heartbeat re-checks add suspects after the held probe,
which must agree with the event
+ detector.laterGatedResult = eventAlive;
+ detector.otherResult = eventAlive;
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ Thread checkThread = new Thread(() -> {
+ try {
+ detector.checkMembers(false);
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ });
+ detector.gatedThread = checkThread;
+ checkThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+
+ // The basic check is verifying the member, which must not be visible
until verified
+ Assert.assertNull("An unverified member must not be in the table",
detector.getMember(member));
+ detector.memberAdded(member);
+ detector.releaseCheck.countDown();
+ checkThread.join(10000);
+
+ Assert.assertFalse("Check thread did not finish",
checkThread.isAlive());
+ Assert.assertNull("Check failed unexpectedly", failure.get());
+ }
+
+ @Test
+ public void testBasicCheckAddsVerifiedMemberOnlyAfterProbe() 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);
+ chain.members = new Member[] { member };
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ Thread checkThread = new Thread(() -> {
+ try {
+ detector.checkMembers(false);
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ });
+ detector.gatedThread = checkThread;
+ checkThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+ Assert.assertNull("An unverified member must not be in the table",
detector.getMember(member));
+ Assert.assertTrue("An unverified member must not be reported",
recorder.added.isEmpty());
+
+ detector.releaseCheck.countDown();
+ checkThread.join(10000);
+
+ Assert.assertFalse("Check thread did not finish",
checkThread.isAlive());
+ Assert.assertNull("Check failed unexpectedly", failure.get());
+ Assert.assertNotNull("A verified member must be added",
detector.getMember(member));
+ Assert.assertEquals("A verified member must be reported once", 1,
recorder.added.size());
+ }
+
+ @Test
+ public void testBasicCheckDeadProbeDoesNotOverrideLaterAdd() throws
Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ GatedAddDetector detector = new GatedAddDetector();
+ StaticMember member = new StaticMember("localhost", GATED_PORT, -1);
+
+ runBasicCheckWithConcurrentAdd(false, true, detector, recorder, chain,
member);
+
+ Assert.assertNotNull("The later add must win over the older dead
probe", detector.getMember(member));
+ Assert.assertEquals("The member must be reported exactly once", 1,
recorder.added.size());
+ }
+
+ @Test
+ public void testBasicCheckLiveProbeDoesNotOverrideLaterAdd() throws
Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ GatedAddDetector detector = new GatedAddDetector();
+ StaticMember member = new StaticMember("localhost", GATED_PORT, -1);
+
+ runBasicCheckWithConcurrentAdd(true, true, detector, recorder, chain,
member);
+
+ Assert.assertNotNull("The member must stay in the table",
detector.getMember(member));
+ Assert.assertEquals("The member must be reported exactly once, by the
later add", 1,
+ recorder.added.size());
+ }
+
+ @Test
+ public void testBasicCheckLiveProbeDoesNotOverrideLaterFailedAdd() throws
Exception {
+ FakeMembershipChain chain = new FakeMembershipChain();
+ RecordingInterceptor recorder = new RecordingInterceptor();
+ GatedAddDetector detector = new GatedAddDetector();
+ StaticMember member = new StaticMember("localhost", GATED_PORT, -1);
+
+ runBasicCheckWithConcurrentAdd(true, false, detector, recorder, chain,
member);
+
+ Assert.assertNull("The later failed add must win over the older live
probe", detector.getMember(member));
+ Assert.assertTrue("A superseded probe must not be reported",
recorder.added.isEmpty());
+ Assert.assertTrue("The failed static member must be an add suspect",
+ detector.addSuspects.containsKey(member));
+ }
+
+ @Test
+ public void testHeartbeatChangesInvalidateInFlightAddCheck() 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);
+ chain.members = new Member[] { member };
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+
+ Thread addThread = new Thread(() -> {
+ try {
+ detector.memberAdded(member);
+ } catch (Throwable t) {
+ failure.set(t);
+ }
+ });
+ detector.gatedThread = addThread;
+ addThread.start();
+ Assert.assertTrue("Liveness check did not start",
+ detector.checkStarted.await(10, TimeUnit.SECONDS));
+
+ // Forced heartbeat checks add the member and then find it dead again,
while the
+ // check started by the member event is still running
+ detector.otherResult = true;
+ detector.checkMembers(true);
+ Assert.assertNotNull("The forced check must add the member",
detector.getMember(member));
+ detector.otherResult = false;
+ detector.checkMembers(true);
+ Assert.assertNull("The forced check must remove the member",
detector.getMember(member));
+
+ detector.releaseCheck.countDown();
+ addThread.join(10000);
+
+ Assert.assertFalse("Member add thread did not finish",
addThread.isAlive());
+ Assert.assertNull("Member add failed unexpectedly", failure.get());
+ Assert.assertNull("A stale live result must not resurrect a member the
heartbeat found dead",
+ detector.getMember(member));
+ Assert.assertEquals("Only the forced check may have reported the
member", 1, recorder.added.size());
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]