package com.twitter.common.zookeeper;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.easymock.Capture;
import org.junit.Before;
import org.junit.Test;
import com.twitter.common.base.Closure;
import com.twitter.common.base.Closures;
import com.twitter.common.base.Command;
import com.twitter.common.testing.easymock.EasyMockTest;
import com.twitter.common.zookeeper.ZooKeeperNode.NodeDeserializer;
import com.twitter.common.zookeeper.testing.BaseZooKeeperTest;
import static org.easymock.EasyMock.aryEq;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
public class ZooKeeperNodeTest {
public static class LightWeightTests extends EasyMockTest {
private ZooKeeperClient zooKeeperClient;
private ZooKeeper zk;
private NodeDeserializer<String> deserializer;
private Closure<String> dataUpdateListener;
private ZooKeeperNode<String> node;
@Before
public void setUp() {
zooKeeperClient = createMock(ZooKeeperClient.class);
zk = createMock(ZooKeeper.class);
deserializer = createMock(new Clazz<NodeDeserializer<String>>() { });
dataUpdateListener = createMock(new Clazz<Closure<String>>() { });
node = new ZooKeeperNode<String>(zooKeeperClient, "/foo", deserializer, dataUpdateListener);
}
@Test
public void testWatchersReused() throws Exception {
// 1st init with initial no node exception
expect(zooKeeperClient.registerExpirationHandler(isA(Command.class))).andReturn(null);
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> dataWatcher1 = createCapture();
expect(zk.getData(eq("/foo"), capture(dataWatcher1), isA(Stat.class)))
.andThrow(new NoNodeException()); // Force an existence watch to be set
dataUpdateListener.execute(null);
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> existenceWatcher1 = createCapture();
expect(zk.exists(eq("/foo"), capture(existenceWatcher1))).andReturn(new Stat());
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> dataWatcher2 = createCapture();
expect(zk.getData(eq("/foo"), capture(dataWatcher2), isA(Stat.class)))
.andReturn("bob".getBytes());
expect(deserializer.deserialize(aryEq("bob".getBytes()), isA(Stat.class))).andReturn("fred");
dataUpdateListener.execute("fred");
// 2nd init with initial no node exception
expect(zooKeeperClient.registerExpirationHandler(isA(Command.class))).andReturn(null);
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> dataWatcher3 = createCapture();
expect(zk.getData(eq("/foo"), capture(dataWatcher3), isA(Stat.class)))
.andThrow(new NoNodeException()); // Force an existence watch to be set
dataUpdateListener.execute(null);
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> existenceWatcher2 = createCapture();
expect(zk.exists(eq("/foo"), capture(existenceWatcher2))).andReturn(new Stat());
expect(zooKeeperClient.get()).andReturn(zk);
Capture<Watcher> dataWatcher4 = createCapture();
expect(zk.getData(eq("/foo"), capture(dataWatcher4), isA(Stat.class)))
.andReturn("bip".getBytes());
expect(deserializer.deserialize(aryEq("bip".getBytes()), isA(Stat.class))).andReturn("frog");
dataUpdateListener.execute("frog");
control.replay();
node.init();
node.init();
assertSame(dataWatcher1.getValue(), dataWatcher2.getValue());
assertSame(dataWatcher2.getValue(), dataWatcher3.getValue());
assertSame(dataWatcher3.getValue(), dataWatcher4.getValue());
assertSame(existenceWatcher1.getValue(), existenceWatcher2.getValue());
}
}
public static class HeavyWeightTests extends BaseZooKeeperTest {
private static final List<ACL> ACL = ZooDefs.Ids.OPEN_ACL_UNSAFE;
private ZooKeeperClient zkClient;
private static class Listener<T> implements Closure<T> {
// We use AtomicReference as a wrapper since LinkedBlockingQueue does not allow null values.
private final BlockingQueue<AtomicReference<T>> queue =
new LinkedBlockingQueue<AtomicReference<T>>();
public void execute(T item) {
queue.offer(new AtomicReference<T>(item));
}
public T waitForUpdate() throws InterruptedException {
return queue.take().get();
}
}
private String nodePath;
@Before
public void mySetUp() throws Exception {
zkClient = createZkClient();
ZooKeeperUtils.ensurePath(zkClient, ACL, "/twitter");
nodePath = "/twitter/node";
}
@Test
public void testZooKeeperUnavailableAtConstruction() throws Exception {
shutdownNetwork(); // Make zk unavailable.
// Should be fine.
makeUninitializedNode(nodePath, Closures.<String>noop());
}
@Test(expected = KeeperException.class)
public void testZooKeeperUnavailableAtInit() throws Exception {
ZooKeeperNode zkNode = makeUninitializedNode(nodePath, Closures.<String>noop());
shutdownNetwork(); // Make zk unavailable.
zkNode.init();
}
@Test
public void testInitialization() throws Exception {
String data = "abcdefg";
zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT);
ZooKeeperNode zkNode = makeUninitializedNode(nodePath, Closures.<String>noop());
// get() should return null before initialization
assertEquals(null, zkNode.get());
zkNode.init();
// Now that init has been called, the data should be synchronously reflected.
assertEquals(data, zkNode.get());
}
@Test
public void testInitialEmptyNode() throws Exception {
Listener<String> listener = new Listener<String>();
ZooKeeperNode<String> zkNode = makeUninitializedNode(nodePath, listener);
assertEquals(null, zkNode.get());
zkNode.init();
assertEquals(null, listener.waitForUpdate());
String data = "abcdefg";
zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT);
assertEquals(data, listener.waitForUpdate());
}
@Test
public void testChangingData() throws Exception {
String data1 = "test_data";
zkClient.get().create(nodePath, data1.getBytes(), ACL, CreateMode.PERSISTENT);
Listener<String> listener = new Listener<String>();
TestDeserializer deserializer = new TestDeserializer();
makeNode(deserializer, nodePath, listener);
assertEquals(data1, listener.waitForUpdate());
assertNotNull(deserializer.getStat());
assertEquals(0, deserializer.getStat().getVersion());
String data2 = "BLAH";
zkClient.get().setData(nodePath, data2.getBytes(), -1);
assertEquals(data2, listener.waitForUpdate());
assertEquals(1, deserializer.getStat().getVersion());
}
@Test
public void testRemoveNode() throws Exception {
String data = "testdata";
zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT);
Listener<String> listener = new Listener<String>();
TestDeserializer deserializer = new TestDeserializer();
makeNode(deserializer, nodePath, listener);
assertEquals(data, listener.waitForUpdate());
assertNotNull(deserializer.getStat());
assertEquals(0, deserializer.getStat().getVersion());
zkClient.get().delete(nodePath, -1);
assertEquals(null, listener.waitForUpdate());
assertEquals(0, deserializer.getStat().getVersion());
zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT);
assertEquals(data, listener.waitForUpdate());
assertEquals(0, deserializer.getStat().getVersion());
}
@Test
public void testSessionExpireLogic() throws Exception {
String data1 = "testdata";
zkClient.get().create(nodePath, data1.getBytes(), ACL, CreateMode.PERSISTENT);
Listener<String> listener = new Listener<String>();
TestDeserializer deserializer = new TestDeserializer();
makeNode(deserializer, nodePath, listener);
assertEquals(data1, listener.waitForUpdate());
assertNotNull(deserializer.getStat());
assertEquals(0, deserializer.getStat().getVersion());
expireSession(zkClient);
assertEquals(data1, listener.waitForUpdate());
String data2 = "avewf";
zkClient = createZkClient();
zkClient.get().setData(nodePath, data2.getBytes(), -1);
assertEquals(data2, listener.waitForUpdate());
assertEquals(1, deserializer.getStat().getVersion());
}
@Test
public void testStaticCreate() throws Exception {
String data = "stuff";
zkClient.get().create(nodePath, data.getBytes(), ACL, CreateMode.PERSISTENT);
ZooKeeperNode<String> zkNode = ZooKeeperNode.create(zkClient, nodePath, new TestDeserializer());
assertEquals(data, zkNode.get());
}
private ZooKeeperNode<String> makeNode(TestDeserializer deserializer, String path,
Closure<String> listener) throws Exception {
ZooKeeperNode<String> zkNode = makeUninitializedNode(deserializer, path, listener);
zkNode.init();
return zkNode;
}
private ZooKeeperNode<String> makeUninitializedNode(String path, Closure<String> listener)
throws Exception {
return makeUninitializedNode(new TestDeserializer(), path, listener);
}
private ZooKeeperNode<String> makeUninitializedNode(
ZooKeeperNode.NodeDeserializer<String> deserializer, String path, Closure<String> listener)
throws Exception {
// we test deserializertionWithPair primarily because it is deserializertionally a proper
// superset of deserializertionWithByteArray
return new ZooKeeperNode<String>(zkClient, path, deserializer, listener);
}
// helper to test Stat population and retrieval
private static final class TestDeserializer implements ZooKeeperNode.NodeDeserializer<String> {
private Stat stat = null;
@Override
public String deserialize(byte[] data, Stat stat) {
this.stat = stat;
return new String(data);
}
Stat getStat() {
return stat;
}
}
}
}