// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or at: // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ================================================================================================= package com.twitter.common.zookeeper; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.collect.Ordering; import com.twitter.common.base.ExceptionalCommand; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.zookeeper.Candidate.Leader; import com.twitter.common.zookeeper.Group.JoinException; import com.twitter.common.zookeeper.testing.BaseZooKeeperTest; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import static org.junit.Assert.*; /** * @author John Sirois */ public class CandidateImplTest extends BaseZooKeeperTest { private static final List<ACL> ACL = ZooDefs.Ids.OPEN_ACL_UNSAFE; private static final String SERVICE = "/twitter/services/puffin_linkhose/leader"; private static final Amount<Integer, Time> TIMEOUT = Amount.of(1, Time.MINUTES); private LinkedBlockingDeque<CandidateImpl> candidateBuffer; @Before public void mySetUp() throws IOException { candidateBuffer = new LinkedBlockingDeque<CandidateImpl>(); } private Group createGroup(ZooKeeperClient zkClient) throws IOException { return new Group(zkClient, ACL, SERVICE); } private class Reign implements Leader { private ExceptionalCommand<JoinException> abdicate; private final CandidateImpl candidate; private final String id; private CountDownLatch defeated = new CountDownLatch(1); Reign(String id, CandidateImpl candidate) { this.id = id; this.candidate = candidate; } @Override public void onElected(ExceptionalCommand<JoinException> abdicate) { candidateBuffer.offerFirst(candidate); this.abdicate = abdicate; } @Override public void onDefeated() { defeated.countDown(); } public void abdicate() throws JoinException { Preconditions.checkState(abdicate != null); abdicate.execute(); } public void expectDefeated() throws InterruptedException { defeated.await(); } @Override public String toString() { return id; } } @Test public void testOfferLeadership() throws Exception { ZooKeeperClient zkClient1 = createZkClient(TIMEOUT); final CandidateImpl candidate1 = new CandidateImpl(createGroup(zkClient1)) { @Override public String toString() { return "Leader1"; } }; ZooKeeperClient zkClient2 = createZkClient(TIMEOUT); final CandidateImpl candidate2 = new CandidateImpl(createGroup(zkClient2)) { @Override public String toString() { return "Leader2"; } }; ZooKeeperClient zkClient3 = createZkClient(TIMEOUT); final CandidateImpl candidate3 = new CandidateImpl(createGroup(zkClient3)) { @Override public String toString() { return "Leader3"; } }; Reign candidate1Reign = new Reign("1", candidate1); Reign candidate2Reign = new Reign("2", candidate2); Reign candidate3Reign = new Reign("3", candidate3); Supplier<Boolean> candidate1Leader = candidate1.offerLeadership(candidate1Reign); Supplier<Boolean> candidate2Leader = candidate2.offerLeadership(candidate2Reign); Supplier<Boolean> candidate3Leader = candidate3.offerLeadership(candidate3Reign); assertTrue("Since initial group join is synchronous, candidate 1 should be the first leader", candidate1Leader.get()); shutdownNetwork(); restartNetwork(); assertTrue("A re-connect without a session expiration should leave the leader elected", candidate1Leader.get()); candidate1Reign.abdicate(); assertSame(candidate1, candidateBuffer.takeLast()); assertFalse(candidate1Leader.get()); // Active abdication should trigger defeat. candidate1Reign.expectDefeated(); CandidateImpl secondCandidate = candidateBuffer.takeLast(); assertTrue("exactly 1 remaining candidate should now be leader: " + secondCandidate + " " + candidateBuffer, candidate2Leader.get() ^ candidate3Leader.get()); if (secondCandidate == candidate2) { expireSession(zkClient2); assertSame(candidate3, candidateBuffer.takeLast()); assertTrue(candidate3Leader.get()); // Passive expiration should trigger defeat. candidate2Reign.expectDefeated(); } else { expireSession(zkClient3); assertSame(candidate2, candidateBuffer.takeLast()); assertTrue(candidate2Leader.get()); // Passive expiration should trigger defeat. candidate3Reign.expectDefeated(); } } @Test public void testCustomJudge() throws Exception { Function<Iterable<String>, String> judge = new Function<Iterable<String>, String>() { @Override public String apply(Iterable<String> input) { return Ordering.natural().max(input); } }; ZooKeeperClient zkClient1 = createZkClient(TIMEOUT); final CandidateImpl candidate1 = new CandidateImpl(createGroup(zkClient1), judge, CandidateImpl.IP_ADDRESS_DATA_SUPPLIER) { @Override public String toString() { return "Leader1"; } }; ZooKeeperClient zkClient2 = createZkClient(TIMEOUT); final CandidateImpl candidate2 = new CandidateImpl(createGroup(zkClient2), judge, CandidateImpl.IP_ADDRESS_DATA_SUPPLIER) { @Override public String toString() { return "Leader2"; } }; Reign candidate1Reign = new Reign("1", candidate1); Reign candidate2Reign = new Reign("2", candidate2); candidate1.offerLeadership(candidate1Reign); assertSame(candidate1, candidateBuffer.takeLast()); Supplier<Boolean> candidate2Leader = candidate2.offerLeadership(candidate2Reign); assertSame(candidate2, candidateBuffer.takeLast()); candidate1Reign.expectDefeated(); assertTrue("Since the judge picks the newest member joining a group as leader candidate 1 " + "should be defeated and candidate 2 leader", candidate2Leader.get()); } @Test public void testCustomDataSupplier() throws Exception { final String suppliedValue = "SAMPLE_DATA"; ZooKeeperClient zkClient1 = createZkClient(TIMEOUT); final CandidateImpl candidate1 = new CandidateImpl(createGroup(zkClient1), CandidateImpl.MOST_RECENT_JUDGE, new Supplier<byte[]>() { @Override public byte[] get() { return suppliedValue.getBytes(); } }) { @Override public String toString() { return "Leader1"; } }; Reign candidate1Reign = new Reign("1", candidate1); Supplier<Boolean> candidate1Leader = candidate1.offerLeadership(candidate1Reign); candidateBuffer.takeLast(); assertTrue(candidate1Leader.get()); assertArrayEquals(candidate1.getLeaderData(), suppliedValue.getBytes()); } }