// ================================================================================================= // 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.collect.ImmutableSet; import com.twitter.common.collections.Pair; import com.twitter.common.zookeeper.testing.BaseZooKeeperTest; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import org.junit.Before; import org.junit.Test; import java.util.AbstractMap.SimpleEntry; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** * @author Adam Samet */ public class ZooKeeperMapTest extends BaseZooKeeperTest { private static final List<ACL> ACL = ZooDefs.Ids.OPEN_ACL_UNSAFE; private static final Function<byte[], String> BYTES_TO_STRING = new Function<byte[], String>() { @Override public String apply(byte[] from) { return new String(from); }}; private ZooKeeperClient zkClient; private BlockingQueue<Pair<String, String>> entryChanges; @Before public void mySetUp() throws Exception { zkClient = createZkClient(); entryChanges = new LinkedBlockingQueue<Pair<String, String>>(); } @Test(expected = KeeperException.NoNodeException.class) public void testMissingPath() throws Exception { makeMap("/twitter/doesnt/exist"); } @Test(expected = KeeperException.class) public void testZooKeeperUnavailableAtConstruction() throws Exception { final String parentPath = "/twitter/path"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); shutdownNetwork(); // Make zk unavailable. makeUninitializedMap(parentPath); } @Test(expected = KeeperException.class) public void testZooKeeperUnavailableAtInit() throws Exception { final String parentPath = "/twitter/path"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); ZooKeeperMap<String> zkMap = makeUninitializedMap(parentPath); shutdownNetwork(); // Make zk unavailable. zkMap.init(); } @Test public void testInitialization() throws Exception { final String parentPath = "/twitter/path"; final String node = "node"; final String nodePath = parentPath + "/" + node; final String data = "abcdefg"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT); ZooKeeperMap<String> zkMap = makeUninitializedMap(parentPath); // Map should be empty before initialization assertTrue(zkMap.isEmpty()); zkMap.init(); // Now that we've initialized, the data should be synchronously reflected. assertFalse(zkMap.isEmpty()); assertEquals(1, zkMap.size()); assertEquals(data, zkMap.get(node)); } @Test public void testEmptyStaticMap() throws Exception { final String parentPath = "/twitter/path"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); Map<String, String> zkMap = makeMap(parentPath); assertEquals(0, zkMap.size()); assertTrue(zkMap.isEmpty()); } @Test public void testStaticMapWithValues() throws Exception { final String parentPath = "/twitter/path"; final String node1 = "node1"; final String node2 = "node2"; final String nodePath1 = parentPath + "/" + node1; final String nodePath2 = parentPath + "/" + node2; final String data1 = "hello World!"; final String data2 = "evrver232&$"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath1, data1.getBytes(), ACL, CreateMode.PERSISTENT); zkClient.get().create(nodePath2, data2.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = makeMap(parentPath); // Test all java.util.Map operations that are implemented. assertTrue(zkMap.containsKey(node1)); assertTrue(zkMap.containsKey(node2)); assertTrue(zkMap.containsValue(data1)); assertTrue(zkMap.containsValue(data2)); assertEquals(ImmutableSet.of(new SimpleEntry<String, String>(node1, data1), new SimpleEntry<String, String>(node2, data2)), zkMap.entrySet()); assertEquals(data1, zkMap.get(node1)); assertEquals(data2, zkMap.get(node2)); assertFalse(zkMap.isEmpty()); assertEquals(ImmutableSet.of(node1, node2), zkMap.keySet()); assertEquals(2, zkMap.size()); } @Test public void testChangingChildren() throws Exception { final String parentPath = "/twitter/path"; final String node1 = "node1"; final String node2 = "node2"; final String nodePath1 = parentPath + "/" + node1; final String nodePath2 = parentPath + "/" + node2; final String data1 = "wefwe"; final String data2 = "rtgrtg"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath1, data1.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = makeMap(parentPath); assertEquals(1, zkMap.size()); assertEquals(data1, zkMap.get(node1)); assertEquals(null, zkMap.get(node2)); // Make sure the map is updated when a child is added. zkClient.get().create(nodePath2, data2.getBytes(), ACL, CreateMode.PERSISTENT); waitForEntryChange(node2, data2); assertEquals(2, zkMap.size()); assertEquals(data1, zkMap.get(node1)); assertEquals(data2, zkMap.get(node2)); // Make sure the map is updated when a child is deleted. zkClient.get().delete(nodePath1, -1); waitForEntryChange(node1, null); assertEquals(1, zkMap.size()); assertEquals(null, zkMap.get(node1)); assertEquals(data2, zkMap.get(node2)); } @Test public void testChangingChildValues() throws Exception { final String parentPath = "/twitter/path"; final String node = "node"; final String nodePath = parentPath + "/" + node; final String data1 = ""; final String data2 = "abc"; final String data3 = "lalala"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath, data1.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = makeMap(parentPath); assertEquals(1, zkMap.size()); assertEquals(data1, zkMap.get(node)); zkClient.get().setData(nodePath, data2.getBytes(), -1); waitForEntryChange(node, data2); assertEquals(1, zkMap.size()); zkClient.get().setData(nodePath, data3.getBytes(), -1); waitForEntryChange(node, data3); assertEquals(1, zkMap.size()); } @Test public void testRemoveParentNode() throws Exception { final String parentPath = "/twitter/path"; final String node = "node"; final String nodePath = parentPath + "/" + node; final String data = "testdata"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = makeMap(parentPath); assertEquals(1, zkMap.size()); assertEquals(data, zkMap.get(node)); zkClient.get().delete(nodePath, -1); zkClient.get().delete(parentPath, -1); waitForEntryChange(node, null); assertEquals(0, zkMap.size()); assertTrue(zkMap.isEmpty()); // Recreate our node, make sure the map observes it. ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT); waitForEntryChange(node, data); } @Test public void testSessionExpireLogic() throws Exception { final String parentPath = "/twitter/path"; final String node1 = "node1"; final String nodePath1 = parentPath + "/" + node1; final String data1 = "testdata"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath1, data1.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = makeMap(parentPath); assertEquals(1, zkMap.size()); assertEquals(data1, zkMap.get(node1)); expireSession(zkClient); assertEquals(1, zkMap.size()); assertEquals(data1, zkMap.get(node1)); final String node2 = "node2"; final String nodePath2 = parentPath + "/" + node2; final String data2 = "testdata2"; zkClient = createZkClient(); zkClient.get().create(nodePath2, data2.getBytes(), ACL, CreateMode.PERSISTENT); waitForEntryChange(node2, data2); assertEquals(2, zkMap.size()); assertEquals(data2, zkMap.get(node2)); } @Test public void testStaticCreate() throws Exception { String parentPath = "/twitter/path"; String node = "node"; String nodePath = parentPath + "/" + node; String data = "DaTa"; ZooKeeperUtils.ensurePath(zkClient, ACL, parentPath); zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT); Map<String, String> zkMap = ZooKeeperMap.create(zkClient, parentPath, BYTES_TO_STRING); assertEquals(1, zkMap.size()); assertEquals(data, zkMap.get(node)); } private void waitForEntryChange(String key, String value) throws Exception { Pair<String, String> expectedEntry = Pair.of(key, value); while (true) { Pair<String, String> nextEntry = entryChanges.take(); if (expectedEntry.equals(nextEntry)) { return; } } } private Map<String, String> makeMap(String path) throws Exception { ZooKeeperMap<String> zkMap = makeUninitializedMap(path); zkMap.init(); return zkMap; } private ZooKeeperMap<String> makeUninitializedMap(String path) throws Exception { return new ZooKeeperMap<String>(zkClient, path, BYTES_TO_STRING) { @Override void putEntry(String key, String value) { super.putEntry(key, value); recordEntryChange(key); } @Override void removeEntry(String key) { super.removeEntry(key); recordEntryChange(key); } private void recordEntryChange(String key) { entryChanges.offer(Pair.of(key, get(key))); } }; } }