Madan Jampani
Committed by Gerrit Code Review

AsyncLeaderElector APIs in support for mastership balancing

Change-Id: Ia235c6a18c54490dc49ca13e2caebf70b750dbc7
...@@ -60,6 +60,16 @@ public class DefaultLeaderElector extends Synchronous<AsyncLeaderElector> implem ...@@ -60,6 +60,16 @@ public class DefaultLeaderElector extends Synchronous<AsyncLeaderElector> implem
60 } 60 }
61 61
62 @Override 62 @Override
63 + public boolean promote(String topic, NodeId nodeId) {
64 + return complete(asyncElector.promote(topic, nodeId));
65 + }
66 +
67 + @Override
68 + public void evict(NodeId nodeId) {
69 + complete(asyncElector.evict(nodeId));
70 + }
71 +
72 + @Override
63 public Leadership getLeadership(String topic) { 73 public Leadership getLeadership(String topic) {
64 return complete(asyncElector.getLeadership(topic)); 74 return complete(asyncElector.getLeadership(topic));
65 } 75 }
......
...@@ -76,6 +76,28 @@ public interface AsyncLeaderElector extends DistributedPrimitive { ...@@ -76,6 +76,28 @@ public interface AsyncLeaderElector extends DistributedPrimitive {
76 CompletableFuture<Boolean> anoint(String topic, NodeId nodeId); 76 CompletableFuture<Boolean> anoint(String topic, NodeId nodeId);
77 77
78 /** 78 /**
79 + * Attempts to evict a node from all leadership elections it is registered for.
80 + * <p>
81 + * If the node is the current leader for a topic, this call will promote the next top candidate
82 + * (if one exists) to leadership.
83 + *
84 + * @param nodeId node instance identifier
85 + * @return CompletableFuture that is completed when the operation is done.
86 + */
87 + CompletableFuture<Void> evict(NodeId nodeId);
88 +
89 + /**
90 + * Attempts to promote a node to top of candidate list without displacing the current leader.
91 + *
92 + * @param topic leadership topic
93 + * @param nodeId instance identifier of the new top candidate
94 + * @return CompletableFuture that is completed with a boolean when the operation is done. Boolean is true if
95 + * node is now the top candidate. This operation can fail (i.e. return false) if the node
96 + * is not registered to run for election for the topic.
97 + */
98 + CompletableFuture<Boolean> promote(String topic, NodeId nodeId);
99 +
100 + /**
79 * Returns the {@link Leadership} for the specified topic. 101 * Returns the {@link Leadership} for the specified topic.
80 * @param topic leadership topic 102 * @param topic leadership topic
81 * @return CompletableFuture that is completed with the current Leadership state of the topic 103 * @return CompletableFuture that is completed with the current Leadership state of the topic
......
...@@ -58,6 +58,25 @@ public interface LeaderElector extends DistributedPrimitive { ...@@ -58,6 +58,25 @@ public interface LeaderElector extends DistributedPrimitive {
58 boolean anoint(String topic, NodeId nodeId); 58 boolean anoint(String topic, NodeId nodeId);
59 59
60 /** 60 /**
61 + * Attempts to promote a node to top of candidate list.
62 + *
63 + * @param topic leadership topic
64 + * @param nodeId instance identifier of the new top candidate
65 + * @return {@code true} if node is now the top candidate. This operation can fail (i.e. return
66 + * {@code false}) if the node is not registered to run for election for the topic.
67 + */
68 + boolean promote(String topic, NodeId nodeId);
69 +
70 + /**
71 + * Attempts to evict a node from all leadership elections it is registered for.
72 + * <p>
73 + * If the node the current leader for a topic, this call will force the next candidate (if one exists)
74 + * to be promoted to leadership.
75 + * @param nodeId node instance identifier
76 + */
77 + void evict(NodeId nodeId);
78 +
79 + /**
61 * Returns the {@link Leadership} for the specified topic. 80 * Returns the {@link Leadership} for the specified topic.
62 * @param topic leadership topic 81 * @param topic leadership topic
63 * @return current Leadership state of the topic 82 * @return current Leadership state of the topic
......
...@@ -128,8 +128,7 @@ public class NewDistributedLeadershipStore ...@@ -128,8 +128,7 @@ public class NewDistributedLeadershipStore
128 128
129 @Override 129 @Override
130 public void removeRegistration(NodeId nodeId) { 130 public void removeRegistration(NodeId nodeId) {
131 - // TODO 131 + leaderElector.evict(nodeId);
132 - throw new UnsupportedOperationException();
133 } 132 }
134 133
135 @Override 134 @Override
...@@ -139,8 +138,7 @@ public class NewDistributedLeadershipStore ...@@ -139,8 +138,7 @@ public class NewDistributedLeadershipStore
139 138
140 @Override 139 @Override
141 public boolean makeTopCandidate(String topic, NodeId nodeId) { 140 public boolean makeTopCandidate(String topic, NodeId nodeId) {
142 - // TODO 141 + return leaderElector.promote(topic, nodeId);
143 - throw new UnsupportedOperationException();
144 } 142 }
145 143
146 @Override 144 @Override
......
...@@ -70,6 +70,18 @@ public class PartitionedAsyncLeaderElector implements AsyncLeaderElector { ...@@ -70,6 +70,18 @@ public class PartitionedAsyncLeaderElector implements AsyncLeaderElector {
70 } 70 }
71 71
72 @Override 72 @Override
73 + public CompletableFuture<Boolean> promote(String topic, NodeId nodeId) {
74 + return getLeaderElector(topic).promote(topic, nodeId);
75 + }
76 +
77 + @Override
78 + public CompletableFuture<Void> evict(NodeId nodeId) {
79 + return CompletableFuture.allOf(getLeaderElectors().stream()
80 + .map(le -> le.evict(nodeId))
81 + .toArray(CompletableFuture[]::new));
82 + }
83 +
84 + @Override
73 public CompletableFuture<Leadership> getLeadership(String topic) { 85 public CompletableFuture<Leadership> getLeadership(String topic) {
74 return getLeaderElector(topic).getLeadership(topic); 86 return getLeaderElector(topic).getLeadership(topic);
75 } 87 }
......
...@@ -20,6 +20,7 @@ import io.atomix.copycat.client.CopycatClient; ...@@ -20,6 +20,7 @@ import io.atomix.copycat.client.CopycatClient;
20 import io.atomix.resource.Resource; 20 import io.atomix.resource.Resource;
21 import io.atomix.resource.ResourceTypeInfo; 21 import io.atomix.resource.ResourceTypeInfo;
22 22
23 +import java.util.List;
23 import java.util.Map; 24 import java.util.Map;
24 import java.util.Set; 25 import java.util.Set;
25 import java.util.concurrent.CompletableFuture; 26 import java.util.concurrent.CompletableFuture;
...@@ -43,6 +44,7 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector> ...@@ -43,6 +44,7 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector>
43 private final Set<Consumer<Change<Leadership>>> leadershipChangeListeners = 44 private final Set<Consumer<Change<Leadership>>> leadershipChangeListeners =
44 Sets.newConcurrentHashSet(); 45 Sets.newConcurrentHashSet();
45 46
47 + public static final String CHANGE_SUBJECT = "changeEvents";
46 private Listener<Change<Leadership>> listener; 48 private Listener<Change<Leadership>> listener;
47 49
48 public AtomixLeaderElector(CopycatClient client, Resource.Options options) { 50 public AtomixLeaderElector(CopycatClient client, Resource.Options options) {
...@@ -57,13 +59,13 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector> ...@@ -57,13 +59,13 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector>
57 @Override 59 @Override
58 public CompletableFuture<AtomixLeaderElector> open() { 60 public CompletableFuture<AtomixLeaderElector> open() {
59 return super.open().thenApply(result -> { 61 return super.open().thenApply(result -> {
60 - client.onEvent("change", this::handleEvent); 62 + client.onEvent(CHANGE_SUBJECT, this::handleEvent);
61 return result; 63 return result;
62 }); 64 });
63 } 65 }
64 66
65 - private void handleEvent(Change<Leadership> change) { 67 + private void handleEvent(List<Change<Leadership>> changes) {
66 - leadershipChangeListeners.forEach(l -> l.accept(change)); 68 + changes.forEach(change -> leadershipChangeListeners.forEach(l -> l.accept(change)));
67 } 69 }
68 70
69 @Override 71 @Override
...@@ -82,6 +84,16 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector> ...@@ -82,6 +84,16 @@ public class AtomixLeaderElector extends Resource<AtomixLeaderElector>
82 } 84 }
83 85
84 @Override 86 @Override
87 + public CompletableFuture<Boolean> promote(String topic, NodeId nodeId) {
88 + return submit(new AtomixLeaderElectorCommands.Promote(topic, nodeId));
89 + }
90 +
91 + @Override
92 + public CompletableFuture<Void> evict(NodeId nodeId) {
93 + return submit(new AtomixLeaderElectorCommands.Evict(nodeId));
94 + }
95 +
96 + @Override
85 public CompletableFuture<Leadership> getLeadership(String topic) { 97 public CompletableFuture<Leadership> getLeadership(String topic) {
86 return submit(new AtomixLeaderElectorCommands.GetLeadership(topic)); 98 return submit(new AtomixLeaderElectorCommands.GetLeadership(topic));
87 } 99 }
......
...@@ -346,6 +346,102 @@ public final class AtomixLeaderElectorCommands { ...@@ -346,6 +346,102 @@ public final class AtomixLeaderElectorCommands {
346 } 346 }
347 347
348 /** 348 /**
349 + * Command for administratively promote a node as top candidate.
350 + */
351 + @SuppressWarnings("serial")
352 + public static class Promote extends ElectionCommand<Boolean> {
353 + private String topic;
354 + private NodeId nodeId;
355 +
356 + public Promote() {
357 + }
358 +
359 + public Promote(String topic, NodeId nodeId) {
360 + this.topic = topic;
361 + this.nodeId = nodeId;
362 + }
363 +
364 + /**
365 + * Returns the topic.
366 + *
367 + * @return The topic
368 + */
369 + public String topic() {
370 + return topic;
371 + }
372 +
373 + /**
374 + * Returns the nodeId to make top candidate.
375 + *
376 + * @return The nodeId
377 + */
378 + public NodeId nodeId() {
379 + return nodeId;
380 + }
381 +
382 + @Override
383 + public String toString() {
384 + return MoreObjects.toStringHelper(getClass())
385 + .add("topic", topic)
386 + .add("nodeId", nodeId)
387 + .toString();
388 + }
389 +
390 + @Override
391 + public void writeObject(BufferOutput<?> buffer, Serializer serializer) {
392 + buffer.writeString(topic);
393 + buffer.writeString(nodeId.toString());
394 + }
395 +
396 + @Override
397 + public void readObject(BufferInput<?> buffer, Serializer serializer) {
398 + topic = buffer.readString();
399 + nodeId = new NodeId(buffer.readString());
400 + }
401 + }
402 +
403 + /**
404 + * Command for administratively evicting a node from all leadership topics.
405 + */
406 + @SuppressWarnings("serial")
407 + public static class Evict extends ElectionCommand<Void> {
408 + private NodeId nodeId;
409 +
410 + public Evict() {
411 + }
412 +
413 + public Evict(NodeId nodeId) {
414 + this.nodeId = nodeId;
415 + }
416 +
417 + /**
418 + * Returns the node identifier.
419 + *
420 + * @return The nodeId
421 + */
422 + public NodeId nodeId() {
423 + return nodeId;
424 + }
425 +
426 + @Override
427 + public String toString() {
428 + return MoreObjects.toStringHelper(getClass())
429 + .add("nodeId", nodeId)
430 + .toString();
431 + }
432 +
433 + @Override
434 + public void writeObject(BufferOutput<?> buffer, Serializer serializer) {
435 + buffer.writeString(nodeId.toString());
436 + }
437 +
438 + @Override
439 + public void readObject(BufferInput<?> buffer, Serializer serializer) {
440 + nodeId = new NodeId(buffer.readString());
441 + }
442 + }
443 +
444 + /**
349 * Map command type resolver. 445 * Map command type resolver.
350 */ 446 */
351 public static class TypeResolver implements SerializableTypeResolver { 447 public static class TypeResolver implements SerializableTypeResolver {
...@@ -359,6 +455,8 @@ public final class AtomixLeaderElectorCommands { ...@@ -359,6 +455,8 @@ public final class AtomixLeaderElectorCommands {
359 registry.register(GetLeadership.class, -866); 455 registry.register(GetLeadership.class, -866);
360 registry.register(Listen.class, -867); 456 registry.register(Listen.class, -867);
361 registry.register(Unlisten.class, -868); 457 registry.register(Unlisten.class, -868);
458 + registry.register(Promote.class, -869);
459 + registry.register(Evict.class, -870);
362 } 460 }
363 } 461 }
364 } 462 }
......
...@@ -43,10 +43,12 @@ import org.onosproject.cluster.Leadership; ...@@ -43,10 +43,12 @@ import org.onosproject.cluster.Leadership;
43 import org.onosproject.cluster.NodeId; 43 import org.onosproject.cluster.NodeId;
44 import org.onosproject.event.Change; 44 import org.onosproject.event.Change;
45 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Anoint; 45 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Anoint;
46 +import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Evict;
46 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetAllLeaderships; 47 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetAllLeaderships;
47 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetElectedTopics; 48 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetElectedTopics;
48 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetLeadership; 49 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.GetLeadership;
49 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Listen; 50 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Listen;
51 +import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Promote;
50 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Run; 52 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Run;
51 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Unlisten; 53 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Unlisten;
52 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Withdraw; 54 import org.onosproject.store.primitives.resources.impl.AtomixLeaderElectorCommands.Withdraw;
...@@ -86,6 +88,8 @@ public class AtomixLeaderElectorState extends ResourceStateMachine ...@@ -86,6 +88,8 @@ public class AtomixLeaderElectorState extends ResourceStateMachine
86 executor.register(Run.class, this::run); 88 executor.register(Run.class, this::run);
87 executor.register(Withdraw.class, this::withdraw); 89 executor.register(Withdraw.class, this::withdraw);
88 executor.register(Anoint.class, this::anoint); 90 executor.register(Anoint.class, this::anoint);
91 + executor.register(Promote.class, this::promote);
92 + executor.register(Evict.class, this::evict);
89 // Queries 93 // Queries
90 executor.register(GetLeadership.class, this::leadership); 94 executor.register(GetLeadership.class, this::leadership);
91 executor.register(GetAllLeaderships.class, this::allLeaderships); 95 executor.register(GetAllLeaderships.class, this::allLeaderships);
...@@ -93,8 +97,16 @@ public class AtomixLeaderElectorState extends ResourceStateMachine ...@@ -93,8 +97,16 @@ public class AtomixLeaderElectorState extends ResourceStateMachine
93 } 97 }
94 98
95 private void notifyLeadershipChange(Leadership previousLeadership, Leadership newLeadership) { 99 private void notifyLeadershipChange(Leadership previousLeadership, Leadership newLeadership) {
96 - Change<Leadership> change = new Change<>(previousLeadership, newLeadership); 100 + notifyLeadershipChanges(Arrays.asList(new Change<>(previousLeadership, newLeadership)));
97 - listeners.values().forEach(listener -> listener.session().publish("change", change)); 101 + }
102 +
103 + private void notifyLeadershipChanges(List<Change<Leadership>> changes) {
104 + if (changes.isEmpty()) {
105 + return;
106 + }
107 + listeners.values()
108 + .forEach(listener -> listener.session()
109 + .publish(AtomixLeaderElector.CHANGE_SUBJECT, changes));
98 } 110 }
99 111
100 @Override 112 @Override
...@@ -206,6 +218,53 @@ public class AtomixLeaderElectorState extends ResourceStateMachine ...@@ -206,6 +218,53 @@ public class AtomixLeaderElectorState extends ResourceStateMachine
206 } 218 }
207 219
208 /** 220 /**
221 + * Applies an {@link AtomixLeaderElectorCommands.Promote} commit.
222 + * @param commit promote commit
223 + * @return {@code true} if changes desired end state is achieved.
224 + */
225 + public boolean promote(Commit<? extends Promote> commit) {
226 + try {
227 + String topic = commit.operation().topic();
228 + NodeId nodeId = commit.operation().nodeId();
229 + Leadership oldLeadership = leadership(topic);
230 + if (oldLeadership == null || !oldLeadership.candidates().contains(nodeId)) {
231 + return false;
232 + }
233 + elections.computeIfPresent(topic, (k, v) -> new ElectionState(v).promote(commit.operation().nodeId()));
234 + Leadership newLeadership = leadership(topic);
235 + if (!Objects.equal(oldLeadership, newLeadership)) {
236 + notifyLeadershipChange(oldLeadership, newLeadership);
237 + }
238 + return true;
239 + } finally {
240 + commit.close();
241 + }
242 + }
243 +
244 + /**
245 + * Applies an {@link AtomixLeaderElectorCommands.Evict} commit.
246 + * @param commit evict commit
247 + */
248 + public void evict(Commit<? extends Evict> commit) {
249 + try {
250 + List<Change<Leadership>> changes = Lists.newLinkedList();
251 + NodeId nodeId = commit.operation().nodeId();
252 + Set<String> topics = Maps.filterValues(elections, e -> e.candidates().contains(nodeId)).keySet();
253 + topics.forEach(topic -> {
254 + Leadership oldLeadership = leadership(topic);
255 + elections.compute(topic, (k, v) -> v.evict(nodeId, termCounter(topic)::incrementAndGet));
256 + Leadership newLeadership = leadership(topic);
257 + if (!Objects.equal(oldLeadership, newLeadership)) {
258 + changes.add(new Change<>(oldLeadership, newLeadership));
259 + }
260 + });
261 + notifyLeadershipChanges(changes);
262 + } finally {
263 + commit.close();
264 + }
265 + }
266 +
267 + /**
209 * Applies an {@link AtomixLeaderElectorCommands.GetLeadership} commit. 268 * Applies an {@link AtomixLeaderElectorCommands.GetLeadership} commit.
210 * @param commit GetLeadership commit 269 * @param commit GetLeadership commit
211 * @return leader 270 * @return leader
...@@ -362,6 +421,31 @@ public class AtomixLeaderElectorState extends ResourceStateMachine ...@@ -362,6 +421,31 @@ public class AtomixLeaderElectorState extends ResourceStateMachine
362 } 421 }
363 } 422 }
364 423
424 + public ElectionState evict(NodeId nodeId, Supplier<Long> termCounter) {
425 + Optional<Registration> registration =
426 + registrations.stream().filter(r -> r.nodeId.equals(nodeId)).findFirst();
427 + if (registration.isPresent()) {
428 + List<Registration> updatedRegistrations =
429 + registrations.stream()
430 + .filter(r -> !r.nodeId().equals(nodeId))
431 + .collect(Collectors.toList());
432 + if (leader.nodeId().equals(nodeId)) {
433 + if (updatedRegistrations.size() > 0) {
434 + return new ElectionState(updatedRegistrations,
435 + updatedRegistrations.get(0),
436 + termCounter.get(),
437 + System.currentTimeMillis());
438 + } else {
439 + return new ElectionState(updatedRegistrations, null, term, termStartTime);
440 + }
441 + } else {
442 + return new ElectionState(updatedRegistrations, leader, term, termStartTime);
443 + }
444 + } else {
445 + return this;
446 + }
447 + }
448 +
365 public boolean isDuplicate(Registration registration) { 449 public boolean isDuplicate(Registration registration) {
366 return registrations.stream().anyMatch(r -> r.sessionId() == registration.sessionId()); 450 return registrations.stream().anyMatch(r -> r.sessionId() == registration.sessionId());
367 } 451 }
...@@ -406,6 +490,23 @@ public class AtomixLeaderElectorState extends ResourceStateMachine ...@@ -406,6 +490,23 @@ public class AtomixLeaderElectorState extends ResourceStateMachine
406 return this; 490 return this;
407 } 491 }
408 } 492 }
493 +
494 + public ElectionState promote(NodeId nodeId) {
495 + Registration registration = registrations.stream()
496 + .filter(r -> r.nodeId().equals(nodeId))
497 + .findFirst()
498 + .orElse(null);
499 + List<Registration> updatedRegistrations = Lists.newLinkedList();
500 + updatedRegistrations.add(registration);
501 + registrations.stream()
502 + .filter(r -> !r.nodeId().equals(nodeId))
503 + .forEach(updatedRegistrations::add);
504 + return new ElectionState(updatedRegistrations,
505 + leader,
506 + term,
507 + termStartTime);
508 +
509 + }
409 } 510 }
410 511
411 @Override 512 @Override
......
...@@ -51,10 +51,10 @@ public class AtomixLeaderElectorTest extends AtomixTestBase { ...@@ -51,10 +51,10 @@ public class AtomixLeaderElectorTest extends AtomixTestBase {
51 public void testRun() throws Throwable { 51 public void testRun() throws Throwable {
52 leaderElectorRunTests(1); 52 leaderElectorRunTests(1);
53 clearTests(); 53 clearTests();
54 -// leaderElectorRunTests(2); 54 + leaderElectorRunTests(2);
55 -// clearTests(); 55 + clearTests();
56 -// leaderElectorRunTests(3); 56 + leaderElectorRunTests(3);
57 -// clearTests(); 57 + clearTests();
58 } 58 }
59 59
60 private void leaderElectorRunTests(int numServers) throws Throwable { 60 private void leaderElectorRunTests(int numServers) throws Throwable {
...@@ -183,6 +183,63 @@ public class AtomixLeaderElectorTest extends AtomixTestBase { ...@@ -183,6 +183,63 @@ public class AtomixLeaderElectorTest extends AtomixTestBase {
183 } 183 }
184 184
185 @Test 185 @Test
186 + public void testPromote() throws Throwable {
187 + leaderElectorPromoteTests(1);
188 + clearTests();
189 + leaderElectorPromoteTests(2);
190 + clearTests();
191 + leaderElectorPromoteTests(3);
192 + clearTests();
193 + }
194 +
195 + private void leaderElectorPromoteTests(int numServers) throws Throwable {
196 + createCopycatServers(numServers);
197 + Atomix client1 = createAtomixClient();
198 + AtomixLeaderElector elector1 = client1.get("test-elector", AtomixLeaderElector.class).join();
199 + Atomix client2 = createAtomixClient();
200 + AtomixLeaderElector elector2 = client2.get("test-elector", AtomixLeaderElector.class).join();
201 + Atomix client3 = createAtomixClient();
202 + AtomixLeaderElector elector3 = client3.get("test-elector", AtomixLeaderElector.class).join();
203 + elector1.run("foo", node1).join();
204 + elector2.run("foo", node2).join();
205 +
206 + LeaderEventListener listener1 = new LeaderEventListener();
207 + elector1.addChangeListener(listener1).join();
208 + LeaderEventListener listener2 = new LeaderEventListener();
209 + elector2.addChangeListener(listener2).join();
210 + LeaderEventListener listener3 = new LeaderEventListener();
211 + elector3.addChangeListener(listener3).join();
212 +
213 + elector3.promote("foo", node3).thenAccept(result -> {
214 + assertFalse(result);
215 + }).join();
216 +
217 + assertFalse(listener1.hasEvent());
218 + assertFalse(listener2.hasEvent());
219 + assertFalse(listener3.hasEvent());
220 +
221 + elector3.run("foo", node3).join();
222 +
223 + listener1.clearEvents();
224 + listener2.clearEvents();
225 + listener3.clearEvents();
226 +
227 + elector3.promote("foo", node3).thenAccept(result -> {
228 + assertTrue(result);
229 + }).join();
230 +
231 + listener1.nextEvent().thenAccept(result -> {
232 + assertEquals(node3, result.newValue().candidates().get(0));
233 + }).join();
234 + listener2.nextEvent().thenAccept(result -> {
235 + assertEquals(node3, result.newValue().candidates().get(0));
236 + }).join();
237 + listener3.nextEvent().thenAccept(result -> {
238 + assertEquals(node3, result.newValue().candidates().get(0));
239 + }).join();
240 + }
241 +
242 + @Test
186 public void testLeaderSessionClose() throws Throwable { 243 public void testLeaderSessionClose() throws Throwable {
187 leaderElectorLeaderSessionCloseTests(1); 244 leaderElectorLeaderSessionCloseTests(1);
188 clearTests(); 245 clearTests();
...@@ -325,6 +382,10 @@ public class AtomixLeaderElectorTest extends AtomixTestBase { ...@@ -325,6 +382,10 @@ public class AtomixLeaderElectorTest extends AtomixTestBase {
325 return !eventQueue.isEmpty(); 382 return !eventQueue.isEmpty();
326 } 383 }
327 384
385 + public void clearEvents() {
386 + eventQueue.clear();
387 + }
388 +
328 public CompletableFuture<Change<Leadership>> nextEvent() { 389 public CompletableFuture<Change<Leadership>> nextEvent() {
329 synchronized (this) { 390 synchronized (this) {
330 if (eventQueue.isEmpty()) { 391 if (eventQueue.isEmpty()) {
......