/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License 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 org.apache.zookeeper.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZKTestCase;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.apache.zookeeper.Quotas;
import org.apache.jute.BinaryInputArchive;
import org.apache.jute.BinaryOutputArchive;
import org.apache.jute.Record;
import org.apache.zookeeper.common.PathTrie;
import java.lang.reflect.*;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class DataTreeTest extends ZKTestCase {
protected static final Logger LOG = LoggerFactory.getLogger(DataTreeTest.class);
private DataTree dt;
@Before
public void setUp() throws Exception {
dt=new DataTree();
}
@After
public void tearDown() throws Exception {
dt=null;
}
/**
* For ZOOKEEPER-1755 - Test race condition when taking dumpEphemerals and
* removing the session related ephemerals from DataTree structure
*/
@Test(timeout = 60000)
public void testDumpEphemerals() throws Exception {
int count = 1000;
long session = 1000;
long zxid = 2000;
final DataTree dataTree = new DataTree();
LOG.info("Create {} zkclient sessions and its ephemeral nodes", count);
createEphemeralNode(session, dataTree, count);
final AtomicBoolean exceptionDuringDumpEphemerals = new AtomicBoolean(
false);
final AtomicBoolean running = new AtomicBoolean(true);
Thread thread = new Thread() {
public void run() {
PrintWriter pwriter = new PrintWriter(new StringWriter());
try {
while (running.get()) {
dataTree.dumpEphemerals(pwriter);
}
} catch (Exception e) {
LOG.error("Received exception while dumpEphemerals!", e);
exceptionDuringDumpEphemerals.set(true);
}
};
};
thread.start();
LOG.debug("Killing {} zkclient sessions and its ephemeral nodes", count);
killZkClientSession(session, zxid, dataTree, count);
running.set(false);
thread.join();
Assert.assertFalse("Should have got exception while dumpEphemerals!",
exceptionDuringDumpEphemerals.get());
}
private void killZkClientSession(long session, long zxid,
final DataTree dataTree, int count) {
for (int i = 0; i < count; i++) {
dataTree.killSession(session + i, zxid);
}
}
private void createEphemeralNode(long session, final DataTree dataTree,
int count) throws NoNodeException, NodeExistsException {
for (int i = 0; i < count; i++) {
dataTree.createNode("/test" + i, new byte[0], null, session + i,
dataTree.getNode("/").stat.getCversion() + 1, 1, 1);
}
}
@Test(timeout = 60000)
public void testRootWatchTriggered() throws Exception {
class MyWatcher implements Watcher{
boolean fired=false;
public void process(WatchedEvent event) {
if(event.getPath().equals("/"))
fired=true;
}
}
MyWatcher watcher=new MyWatcher();
// set a watch on the root node
dt.getChildren("/", new Stat(), watcher);
// add a new node, should trigger a watch
dt.createNode("/xyz", new byte[0], null, 0, dt.getNode("/").stat.getCversion()+1, 1, 1);
Assert.assertFalse("Root node watch not triggered",!watcher.fired);
}
/**
* For ZOOKEEPER-1046 test if cversion is getting incremented correctly.
*/
@Test(timeout = 60000)
public void testIncrementCversion() throws Exception {
dt.createNode("/test", new byte[0], null, 0, dt.getNode("/").stat.getCversion()+1, 1, 1);
DataNode zk = dt.getNode("/test");
int prevCversion = zk.stat.getCversion();
long prevPzxid = zk.stat.getPzxid();
dt.setCversionPzxid("/test/", prevCversion + 1, prevPzxid + 1);
int newCversion = zk.stat.getCversion();
long newPzxid = zk.stat.getPzxid();
Assert.assertTrue("<cversion, pzxid> verification failed. Expected: <" +
(prevCversion + 1) + ", " + (prevPzxid + 1) + ">, found: <" +
newCversion + ", " + newPzxid + ">",
(newCversion == prevCversion + 1 && newPzxid == prevPzxid + 1));
}
@Test(timeout = 60000)
public void testPathTrieClearOnDeserialize() throws Exception {
//Create a DataTree with quota nodes so PathTrie get updated
DataTree dserTree = new DataTree();
dserTree.createNode("/bug", new byte[20], null, -1, 1, 1, 1);
dserTree.createNode(Quotas.quotaZookeeper+"/bug", null, null, -1, 1, 1, 1);
dserTree.createNode(Quotas.quotaPath("/bug"), new byte[20], null, -1, 1, 1, 1);
dserTree.createNode(Quotas.statPath("/bug"), new byte[20], null, -1, 1, 1, 1);
//deserialize a DataTree; this should clear the old /bug nodes and pathTrie
DataTree tree = new DataTree();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive oa = BinaryOutputArchive.getArchive(baos);
tree.serialize(oa, "test");
baos.flush();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
BinaryInputArchive ia = BinaryInputArchive.getArchive(bais);
dserTree.deserialize(ia, "test");
Field pfield = DataTree.class.getDeclaredField("pTrie");
pfield.setAccessible(true);
PathTrie pTrie = (PathTrie)pfield.get(dserTree);
//Check that the node path is removed from pTrie
Assert.assertEquals("/bug is still in pTrie", "", pTrie.findMaxPrefix("/bug"));
}
/*
* ZOOKEEPER-2201 - OutputArchive.writeRecord can block for long periods of
* time, we must call it outside of the node lock.
* We call tree.serialize, which calls our modified writeRecord method that
* blocks until it can verify that a separate thread can lock the DataNode
* currently being written, i.e. that DataTree.serializeNode does not hold
* the DataNode lock while calling OutputArchive.writeRecord.
*/
@Test(timeout = 60000)
public void testSerializeDoesntLockDataNodeWhileWriting() throws Exception {
DataTree tree = new DataTree();
tree.createNode("/marker", new byte[] {42}, null, -1, 1, 1, 1);
final DataNode markerNode = tree.getNode("/marker");
final AtomicBoolean ranTestCase = new AtomicBoolean();
DataOutputStream out = new DataOutputStream(new ByteArrayOutputStream());
BinaryOutputArchive oa = new BinaryOutputArchive(out) {
@Override
public void writeRecord(Record r, String tag) throws IOException {
// Need check if the record is a DataNode instance because of changes in ZOOKEEPER-2014
// which adds default ACL to config node.
if (r instanceof DataNode) {
DataNode node = (DataNode) r;
if (node.data.length == 1 && node.data[0] == 42) {
final Semaphore semaphore = new Semaphore(0);
new Thread(new Runnable() {
@Override
public void run() {
synchronized (markerNode) {
//When we lock markerNode, allow writeRecord to continue
semaphore.release();
}
}
}).start();
try {
boolean acquired = semaphore.tryAcquire(30, TimeUnit.SECONDS);
//This is the real assertion - could another thread lock
//the DataNode we're currently writing
Assert.assertTrue("Couldn't acquire a lock on the DataNode while we were calling tree.serialize", acquired);
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
}
ranTestCase.set(true);
}
}
super.writeRecord(r, tag);
}
};
tree.serialize(oa, "test");
//Let's make sure that we hit the code that ran the real assertion above
Assert.assertTrue("Didn't find the expected node", ranTestCase.get());
}
}