// ================================================================================================= // 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.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import org.apache.zookeeper.ZooDefs.Ids; import org.junit.Before; import org.junit.Test; import com.twitter.common.base.Command; import com.twitter.common.base.Supplier; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.testing.easymock.EasyMockTest; import com.twitter.common.zookeeper.Group.GroupChangeListener; import com.twitter.common.zookeeper.Group.JoinException; import com.twitter.common.zookeeper.Group.Membership; import com.twitter.common.zookeeper.Group.NodeScheme; import com.twitter.common.zookeeper.ZooKeeperClient.Credentials; import com.twitter.common.zookeeper.testing.BaseZooKeeperTest; import static com.google.common.testing.junit4.JUnitAsserts.assertNotEqual; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.reset; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public class GroupTest extends BaseZooKeeperTest { private ZooKeeperClient zkClient; private Group joinGroup; private Group watchGroup; private Command stopWatching; private com.twitter.common.base.Command onLoseMembership; private RecordingListener listener; public GroupTest() { super(Amount.of(1, Time.DAYS)); } @Before public void mySetUp() throws Exception { onLoseMembership = createMock(Command.class); zkClient = createZkClient("group", "test"); joinGroup = new Group(zkClient, ZooKeeperUtils.EVERYONE_READ_CREATOR_ALL, "/a/group"); watchGroup = new Group(zkClient, ZooKeeperUtils.EVERYONE_READ_CREATOR_ALL, "/a/group"); listener = new RecordingListener(); stopWatching = watchGroup.watch(listener); } private static class RecordingListener implements GroupChangeListener { private final LinkedBlockingQueue<Iterable<String>> membershipChanges = new LinkedBlockingQueue<Iterable<String>>(); @Override public void onGroupChange(Iterable<String> memberIds) { membershipChanges.add(memberIds); } public Iterable<String> take() throws InterruptedException { return membershipChanges.take(); } public void assertEmpty() { assertEquals(ImmutableList.<Iterable<String>>of(), ImmutableList.copyOf(membershipChanges)); } @Override public String toString() { return membershipChanges.toString(); } } private static class CustomScheme implements NodeScheme { static final String NODE_NAME = "custom_name"; @Override public boolean isMember(String nodeName) { return NODE_NAME.equals(nodeName); } @Override public String createName(byte[] membershipData) { return NODE_NAME; } @Override public boolean isSequential() { return false; } } @Test public void testSessionExpirationTriggersOnLoseMembership() throws Exception { final CountDownLatch lostMembership = new CountDownLatch(1); Command onLoseMembership = new Command() { @Override public void execute() throws RuntimeException { lostMembership.countDown(); } }; assertEmptyMembershipObserved(); Membership membership = joinGroup.join(onLoseMembership); assertMembershipObserved(membership.getMemberId()); expireSession(zkClient); lostMembership.await(); // Will hang this test if onLoseMembership event is not propagated. } @Test public void testNodeDeleteTriggersOnLoseMembership() throws Exception { final CountDownLatch lostMembership = new CountDownLatch(1); Command onLoseMembership = new Command() { @Override public void execute() throws RuntimeException { lostMembership.countDown(); } }; assertEmptyMembershipObserved(); Membership membership = joinGroup.join(onLoseMembership); assertMembershipObserved(membership.getMemberId()); membership.cancel(); lostMembership.await(); // Will hang this test if onLoseMembership event is not propagated. } @Test public void testJoinsAndWatchesSurviveDisconnect() throws Exception { replay(onLoseMembership); assertEmptyMembershipObserved(); Membership membership = joinGroup.join(); String originalMemberId = membership.getMemberId(); assertMembershipObserved(originalMemberId); shutdownNetwork(); restartNetwork(); // The member should still be present under existing ephemeral node since session did not // expire. watchGroup.watch(listener); assertMembershipObserved(originalMemberId); membership.cancel(); assertEmptyMembershipObserved(); assertEmptyMembershipObserved(); // and again for 2nd listener listener.assertEmpty(); verify(onLoseMembership); reset(onLoseMembership); // Turn off expectations during ZK server shutdown. } @Test public void testJoinsAndWatchesSurviveExpiredSession() throws Exception { onLoseMembership.execute(); replay(onLoseMembership); assertEmptyMembershipObserved(); Membership membership = joinGroup.join(onLoseMembership); String originalMemberId = membership.getMemberId(); assertMembershipObserved(originalMemberId); expireSession(zkClient); // We should have lost our group membership and then re-gained it with a new ephemeral node. // We may or may-not see the intermediate state change but we must see the final state Iterable<String> members = listener.take(); if (Iterables.isEmpty(members)) { members = listener.take(); } assertEquals(1, Iterables.size(members)); assertNotEqual(originalMemberId, Iterables.getOnlyElement(members)); assertNotEqual(originalMemberId, membership.getMemberId()); listener.assertEmpty(); verify(onLoseMembership); reset(onLoseMembership); // Turn off expectations during ZK server shutdown. } @Test public void testJoinCustomNamingScheme() throws Exception { Group group = new Group(zkClient, ZooKeeperUtils.EVERYONE_READ_CREATOR_ALL, "/a/group", new CustomScheme()); listener = new RecordingListener(); group.watch(listener); assertEmptyMembershipObserved(); Membership membership = group.join(); String memberId = membership.getMemberId(); assertEquals("Wrong member ID.", CustomScheme.NODE_NAME, memberId); assertMembershipObserved(memberId); expireSession(zkClient); } @Test public void testUpdateMembershipData() throws Exception { Supplier<byte[]> dataSupplier = new EasyMockTest.Clazz<Supplier<byte[]>>() {}.createMock(); byte[] initial = "start".getBytes(); expect(dataSupplier.get()).andReturn(initial); byte[] second = "update".getBytes(); expect(dataSupplier.get()).andReturn(second); replay(dataSupplier); Membership membership = joinGroup.join(dataSupplier, onLoseMembership); assertArrayEquals("Initial setting is incorrect.", initial, zkClient.get() .getData(membership.getMemberPath(), false, null)); assertArrayEquals("Updating supplier should not change membership data", initial, zkClient.get().getData(membership.getMemberPath(), false, null)); membership.updateMemberData(); assertArrayEquals("Updating membership should change data", second, zkClient.get().getData(membership.getMemberPath(), false, null)); verify(dataSupplier); } @Test public void testAcls() throws Exception { Group securedMembership = new Group(createZkClient("secured", "group"), ZooKeeperUtils.EVERYONE_READ_CREATOR_ALL, "/secured/group/membership"); String memberId = securedMembership.join().getMemberId(); Group unauthenticatedObserver = new Group(createZkClient(Credentials.NONE), Ids.READ_ACL_UNSAFE, "/secured/group/membership"); RecordingListener unauthenticatedListener = new RecordingListener(); unauthenticatedObserver.watch(unauthenticatedListener); assertMembershipObserved(unauthenticatedListener, memberId); try { unauthenticatedObserver.join(); fail("Expected join exception for unauthenticated observer"); } catch (JoinException e) { // expected } Group unauthorizedObserver = new Group(createZkClient("joe", "schmoe"), Ids.READ_ACL_UNSAFE, "/secured/group/membership"); RecordingListener unauthorizedListener = new RecordingListener(); unauthorizedObserver.watch(unauthorizedListener); assertMembershipObserved(unauthorizedListener, memberId); try { unauthorizedObserver.join(); fail("Expected join exception for unauthorized observer"); } catch (JoinException e) { // expected } } @Test public void testStopWatching() throws Exception { replay(onLoseMembership); assertEmptyMembershipObserved(); Membership member1 = joinGroup.join(); String memberId1 = member1.getMemberId(); assertMembershipObserved(memberId1); Membership member2 = joinGroup.join(); String memberId2 = member2.getMemberId(); assertMembershipObserved(memberId1, memberId2); stopWatching.execute(); member1.cancel(); Membership member3 = joinGroup.join(); member2.cancel(); member3.cancel(); listener.assertEmpty(); } private void assertEmptyMembershipObserved() throws InterruptedException { assertMembershipObserved(); } private void assertMembershipObserved(String... expectedMemberIds) throws InterruptedException { assertMembershipObserved(listener, expectedMemberIds); } private void assertMembershipObserved(RecordingListener listener, String... expectedMemberIds) throws InterruptedException { assertEquals(ImmutableSet.copyOf(expectedMemberIds), ImmutableSet.copyOf(listener.take())); } }