// ================================================================================================= // 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 java.io.IOException; import java.net.InetSocketAddress; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.testing.TearDown; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher.Event.EventType; import org.apache.zookeeper.Watcher.Event.KeeperState; import org.easymock.Capture; import org.easymock.EasyMock; import org.easymock.IAnswer; import org.easymock.IExpectationSetters; import org.easymock.IMocksControl; import org.junit.Before; import org.junit.Test; import com.twitter.common.base.ExceptionalCommand; import com.twitter.common.zookeeper.Candidate.Leader; import com.twitter.common.zookeeper.Group.JoinException; import com.twitter.common.zookeeper.ServerSet.EndpointStatus; import com.twitter.common.zookeeper.SingletonService.DefeatOnDisconnectLeader; import com.twitter.common.zookeeper.SingletonService.LeaderControl; import com.twitter.common.zookeeper.SingletonService.LeadershipListener; import com.twitter.common.zookeeper.testing.BaseZooKeeperTest; import static com.twitter.common.testing.easymock.EasyMockTest.createCapture; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.createControl; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.fail; public class SingletonServiceTest extends BaseZooKeeperTest { private static final int PORT_A = 1234; private static final int PORT_B = 8080; private static final InetSocketAddress PRIMARY_ENDPOINT = InetSocketAddress.createUnresolved("foo", PORT_A); private static final Map<String, InetSocketAddress> AUX_ENDPOINTS = ImmutableMap.of("http-admin", InetSocketAddress.createUnresolved("foo", PORT_B)); private IMocksControl control; private SingletonService.LeadershipListener listener; private ServerSet serverSet; private ServerSet.EndpointStatus endpointStatus; private Candidate candidate; private ExceptionalCommand<Group.JoinException> abdicate; private SingletonService service; @Before @SuppressWarnings("unchecked") public void mySetUp() throws IOException { control = createControl(); addTearDown(new TearDown() { @Override public void tearDown() { control.verify(); } }); listener = control.createMock(SingletonService.LeadershipListener.class); serverSet = control.createMock(ServerSet.class); candidate = control.createMock(Candidate.class); endpointStatus = control.createMock(ServerSet.EndpointStatus.class); abdicate = control.createMock(ExceptionalCommand.class); service = new SingletonService(serverSet, candidate); } private void newLeader( final String hostName, Capture<Leader> leader, LeadershipListener listener) throws Exception { service.lead(InetSocketAddress.createUnresolved(hostName, PORT_A), ImmutableMap.of("http-admin", InetSocketAddress.createUnresolved(hostName, PORT_B)), listener); // This actually elects the leader. leader.getValue().onElected(abdicate); } private void newLeader(String hostName, Capture<Leader> leader) throws Exception { newLeader(hostName, leader, listener); } private IExpectationSetters<EndpointStatus> expectJoin() throws Exception { return expect(serverSet.join(PRIMARY_ENDPOINT, AUX_ENDPOINTS)); } @Test public void testLeadAdvertise() throws Exception { Capture<Leader> leaderCapture = createCapture(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); expectJoin().andReturn(endpointStatus); endpointStatus.leave(); abdicate.execute(); control.replay(); newLeader("foo", leaderCapture); controlCapture.getValue().advertise(); controlCapture.getValue().leave(); } @Test public void teatLeadLeaveNoAdvertise() throws Exception { Capture<Leader> leaderCapture = createCapture(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); abdicate.execute(); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); control.replay(); newLeader("foo", leaderCapture); controlCapture.getValue().leave(); } @Test public void testLeadJoinFailure() throws Exception { Capture<Leader> leaderCapture = new Capture<Leader>(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); expectJoin().andThrow(new Group.JoinException("Injected join failure.", new Exception())); abdicate.execute(); control.replay(); newLeader("foo", leaderCapture); try { controlCapture.getValue().advertise(); fail("Join should have failed."); } catch (JoinException e) { // Expected. } controlCapture.getValue().leave(); } @Test(expected = IllegalStateException.class) public void testMultipleAdvertise() throws Exception { Capture<Leader> leaderCapture = createCapture(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); expectJoin().andReturn(endpointStatus); control.replay(); newLeader("foo", leaderCapture); controlCapture.getValue().advertise(); controlCapture.getValue().advertise(); } @Test(expected = IllegalStateException.class) public void testMultipleLeave() throws Exception { Capture<Leader> leaderCapture = createCapture(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); expectJoin().andReturn(endpointStatus); endpointStatus.leave(); abdicate.execute(); control.replay(); newLeader("foo", leaderCapture); controlCapture.getValue().advertise(); controlCapture.getValue().leave(); controlCapture.getValue().leave(); } @Test(expected = IllegalStateException.class) public void testAdvertiseAfterLeave() throws Exception { Capture<Leader> leaderCapture = createCapture(); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); Capture<LeaderControl> controlCapture = createCapture(); listener.onLeading(capture(controlCapture)); abdicate.execute(); control.replay(); newLeader("foo", leaderCapture); controlCapture.getValue().leave(); controlCapture.getValue().advertise(); } @Test public void testLeadMulti() throws Exception { List<Capture<Leader>> leaderCaptures = Lists.newArrayList(); List<Capture<LeaderControl>> leaderControlCaptures = Lists.newArrayList(); for (int i = 0; i < 5; i++) { Capture<Leader> leaderCapture = new Capture<Leader>(); leaderCaptures.add(leaderCapture); Capture<LeaderControl> controlCapture = createCapture(); leaderControlCaptures.add(controlCapture); expect(candidate.offerLeadership(capture(leaderCapture))).andReturn(null); listener.onLeading(capture(controlCapture)); InetSocketAddress primary = InetSocketAddress.createUnresolved("foo" + i, PORT_A); Map<String, InetSocketAddress> aux = ImmutableMap.of("http-admin", InetSocketAddress.createUnresolved("foo" + i, PORT_B)); expect(serverSet.join(primary, aux)).andReturn(endpointStatus); endpointStatus.leave(); abdicate.execute(); } control.replay(); for (int i = 0; i < 5; i++) { final String leaderName = "foo" + i; newLeader(leaderName, leaderCaptures.get(i)); leaderControlCaptures.get(i).getValue().advertise(); leaderControlCaptures.get(i).getValue().leave(); } } @Test public void testLeaderLeaves() throws Exception { control.replay(); shutdownNetwork(); } private static IAnswer<?> countDownAnswer(final CountDownLatch latch) { return new IAnswer<Void>() { @Override public Void answer() { latch.countDown(); return null; } }; } @Test public void testLeaderDisconnect() throws Exception { Capture<LeaderControl> controlCapture = createCapture(); CountDownLatch leading = new CountDownLatch(1); listener.onLeading(capture(controlCapture)); expectLastCall().andAnswer(countDownAnswer(leading)); CountDownLatch defeated = new CountDownLatch(1); listener.onDefeated(null); expectLastCall().andAnswer(countDownAnswer(defeated)); control.replay(); ZooKeeperClient zkClient = createZkClient(); serverSet = new ServerSetImpl(zkClient, "/fake/path"); candidate = new CandidateImpl( new Group(zkClient, ZooKeeperUtils.OPEN_ACL_UNSAFE, "/fake/path")); DefeatOnDisconnectLeader leader = new DefeatOnDisconnectLeader(zkClient, listener); service = new SingletonService(serverSet, candidate); service.lead(InetSocketAddress.createUnresolved("foo", PORT_A), ImmutableMap.of("http-admin", InetSocketAddress.createUnresolved("foo", PORT_B)), leader); leading.await(); shutdownNetwork(); defeated.await(); } @Test public void testNonLeaderDisconnect() throws Exception { CountDownLatch elected = new CountDownLatch(1); listener.onLeading(EasyMock.<LeaderControl>anyObject()); expectLastCall().andAnswer(countDownAnswer(elected)); listener.onDefeated(null); expectLastCall().anyTimes(); control.replay(); ZooKeeperClient zkClient = createZkClient(); String path = "/fake/path"; // Create a fake leading candidate node to ensure that the leader in this test is never // elected. ZooKeeperUtils.ensurePath(zkClient, ZooKeeperUtils.OPEN_ACL_UNSAFE, path); String leaderNode = zkClient.get().create( path + "/" + SingletonService.LEADER_ELECT_NODE_PREFIX, "fake_leader".getBytes(), ZooKeeperUtils.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); serverSet = new ServerSetImpl(zkClient, path); candidate = SingletonService.createSingletonCandidate(zkClient, path, ZooKeeperUtils.OPEN_ACL_UNSAFE); DefeatOnDisconnectLeader leader = new DefeatOnDisconnectLeader(zkClient, listener); service = new SingletonService(serverSet, candidate); service.lead(InetSocketAddress.createUnresolved("foo", PORT_A), ImmutableMap.of("http-admin", InetSocketAddress.createUnresolved("foo", PORT_B)), leader); final CountDownLatch disconnected = new CountDownLatch(1); zkClient.register(new Watcher() { @Override public void process(WatchedEvent event) { if ((event.getType() == EventType.None) && (event.getState() == KeeperState.Disconnected)) { disconnected.countDown(); } } }); shutdownNetwork(); disconnected.await(); restartNetwork(); zkClient.get().delete(leaderNode, ZooKeeperUtils.ANY_VERSION); // Upon deletion of the fake leader node, the candidate should become leader. elected.await(); } }