You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@curator.apache.org by ra...@apache.org on 2014/08/08 04:09:10 UTC
[3/9] git commit: Added more extensive testing.
Added more extensive testing.
Project: http://git-wip-us.apache.org/repos/asf/curator/repo
Commit: http://git-wip-us.apache.org/repos/asf/curator/commit/f4743336
Tree: http://git-wip-us.apache.org/repos/asf/curator/tree/f4743336
Diff: http://git-wip-us.apache.org/repos/asf/curator/diff/f4743336
Branch: refs/heads/master
Commit: f4743336e09fa4f487b95bf72b2877c789371202
Parents: cf700d3
Author: Scott Blum <sc...@squareup.com>
Authored: Fri Aug 1 02:00:52 2014 -0400
Committer: Scott Blum <sc...@squareup.com>
Committed: Fri Aug 1 02:00:52 2014 -0400
----------------------------------------------------------------------
.../framework/recipes/cache/TreeCache.java | 113 ++++++----
.../framework/recipes/cache/TreeCacheEvent.java | 10 +-
.../recipes/cache/BaseTestTreeCache.java | 173 +++++++++++++++
.../framework/recipes/cache/TestTreeCache.java | 219 +++----------------
.../recipes/cache/TestTreeCacheRandomTree.java | 199 +++++++++++++++++
5 files changed, 484 insertions(+), 230 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/curator/blob/f4743336/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCache.java
----------------------------------------------------------------------
diff --git a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCache.java b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCache.java
index 4781253..f73861d 100644
--- a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCache.java
+++ b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCache.java
@@ -20,7 +20,8 @@
package org.apache.curator.framework.recipes.cache;
import com.google.common.base.Function;
-import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.BackgroundCallback;
@@ -38,15 +39,15 @@ import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.SortedSet;
+import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -68,7 +69,7 @@ public class TreeCache implements Closeable
PENDING, LIVE, DEAD
}
- final class TreeNode implements Watcher, BackgroundCallback
+ private final class TreeNode implements Watcher, BackgroundCallback
{
private final AtomicReference<NodeState> nodeState = new AtomicReference<NodeState>(NodeState.PENDING);
private final String path;
@@ -77,12 +78,18 @@ public class TreeCache implements Closeable
private final AtomicReference<byte[]> data = new AtomicReference<byte[]>();
private final AtomicReference<ConcurrentMap<String, TreeNode>> children = new AtomicReference<ConcurrentMap<String, TreeNode>>();
- TreeNode(String path, TreeNode parent)
+ private TreeNode(String path, TreeNode parent)
{
this.path = path;
this.parent = parent;
}
+ private void refresh() throws Exception
+ {
+ refreshData();
+ refreshChildren();
+ }
+
private void refreshChildren() throws Exception
{
outstandingOps.incrementAndGet();
@@ -104,8 +111,7 @@ public class TreeCache implements Closeable
private void wasReconnected() throws Exception
{
- refreshData();
- refreshChildren();
+ refresh();
ConcurrentMap<String, TreeNode> childMap = children.get();
if ( childMap != null )
{
@@ -118,8 +124,7 @@ public class TreeCache implements Closeable
private void wasCreated() throws Exception
{
- refreshData();
- refreshChildren();
+ refresh();
}
private void wasDeleted() throws Exception
@@ -172,7 +177,7 @@ public class TreeCache implements Closeable
switch ( event.getType() )
{
case NodeCreated:
- assert parent == null;
+ Preconditions.checkState(parent == null, "unexpected NodeCreated on non-root node");
wasCreated();
break;
case NodeChildrenChanged:
@@ -198,7 +203,7 @@ public class TreeCache implements Closeable
switch ( event.getType() )
{
case EXISTS:
- // TODO: should only happen for root node
+ Preconditions.checkState(parent == null, "unexpected EXISTS on non-root node");
if ( event.getResultCode() == KeeperException.Code.OK.intValue() )
{
nodeState.compareAndSet(NodeState.DEAD, NodeState.PENDING);
@@ -284,7 +289,7 @@ public class TreeCache implements Closeable
if ( outstandingOps.decrementAndGet() == 0 )
{
- if ( treeState.compareAndSet(TreeState.LATENT, TreeState.STARTED) )
+ if ( isInitialized.compareAndSet(false, true) )
{
publishEvent(TreeCacheEvent.Type.INITIALIZED);
}
@@ -300,10 +305,15 @@ public class TreeCache implements Closeable
}
/**
- * Detemines when to publish the initialized event.
+ * Tracks the number of outstanding background requests in flight. The first time this count reaches 0, we publish the initialized event.
*/
private final AtomicLong outstandingOps = new AtomicLong(0);
+ /**
+ * Have we published the {@link TreeCacheEvent.Type#INITIALIZED} event yet?
+ */
+ private final AtomicBoolean isInitialized = new AtomicBoolean(false);
+
private final TreeNode root;
private final CuratorFramework client;
private final CloseableExecutorService executorService;
@@ -391,17 +401,16 @@ public class TreeCache implements Closeable
*/
public void start() throws Exception
{
+ Preconditions.checkState(treeState.compareAndSet(TreeState.LATENT, TreeState.STARTED), "already started");
client.getConnectionStateListenable().addListener(connectionStateListener);
root.wasCreated();
}
/**
- * Close/end the cache
- *
- * @throws java.io.IOException errors
+ * Close/end the cache.
*/
@Override
- public void close() throws IOException
+ public void close()
{
if ( treeState.compareAndSet(TreeState.STARTED, TreeState.CLOSED) )
{
@@ -439,7 +448,11 @@ public class TreeCache implements Closeable
TreeNode current = root;
if ( fullPath.length() > root.path.length() )
{
- List<String> split = ZKPaths.split(fullPath.substring(root.path.length()));
+ if ( root.path.length() > 1 )
+ {
+ fullPath = fullPath.substring(root.path.length());
+ }
+ List<String> split = ZKPaths.split(fullPath);
for ( String part : split )
{
ConcurrentMap<String, TreeNode> map = current.children.get();
@@ -458,14 +471,14 @@ public class TreeCache implements Closeable
}
/**
- * Return the current set of children. There are no guarantees of accuracy. This is
- * merely the most recent view of the data. The data is returned in sorted order. If there is
- * no child with that path, <code>null</code> is returned.
+ * Return the current set of children at the given path, mapped by child name. There are no
+ * guarantees of accuracy; this is merely the most recent view of the data. If there is no
+ * node at this path, {@code null} is returned.
*
* @param fullPath full path to the node to check
* @return a possibly-empty list of children if the node is alive, or null
*/
- public SortedSet<String> getCurrentChildren(String fullPath)
+ public Map<String, ChildData> getCurrentChildren(String fullPath)
{
TreeNode node = find(fullPath);
if ( node == null || node.nodeState.get() != NodeState.LIVE )
@@ -473,14 +486,25 @@ public class TreeCache implements Closeable
return null;
}
ConcurrentMap<String, TreeNode> map = node.children.get();
- SortedSet<String> result;
+ Map<String, ChildData> result;
if ( map == null )
{
- result = ImmutableSortedSet.of();
+ result = ImmutableMap.of();
}
else
{
- result = ImmutableSortedSet.copyOf(map.keySet());
+ ImmutableMap.Builder<String, ChildData> builder = ImmutableMap.builder();
+ for ( Map.Entry<String, TreeNode> entry : map.entrySet() )
+ {
+ TreeNode childNode = entry.getValue();
+ ChildData childData = new ChildData(childNode.path, childNode.stat.get(), childNode.data.get());
+ // Double-check liveness after retreiving data.
+ if ( childNode.nodeState.get() == NodeState.LIVE )
+ {
+ builder.put(entry.getKey(), childData);
+ }
+ }
+ result = builder.build();
}
// Double-check liveness after retreiving children.
@@ -489,8 +513,8 @@ public class TreeCache implements Closeable
/**
* Return the current data for the given path. There are no guarantees of accuracy. This is
- * merely the most recent view of the data. If there is no child with that path,
- * <code>null</code> is returned.
+ * merely the most recent view of the data. If there is no node at the given path,
+ * {@code null} is returned.
*
* @param fullPath full path to the node to check
* @return data if the node is alive, or null
@@ -503,29 +527,28 @@ public class TreeCache implements Closeable
return null;
}
ChildData result = new ChildData(node.path, node.stat.get(), node.data.get());
- // Double-check liveness after retreiving stat / data.
+ // Double-check liveness after retreiving data.
return node.nodeState.get() == NodeState.LIVE ? result : null;
}
- void callListeners(final TreeCacheEvent event)
+ private void callListeners(final TreeCacheEvent event)
{
listeners.forEach(new Function<TreeCacheListener, Void>()
- {
- @Override
- public Void apply(TreeCacheListener listener)
- {
- try
- {
- listener.childEvent(client, event);
- }
- catch ( Exception e )
- {
- handleException(e);
- }
- return null;
- }
- }
- );
+ {
+ @Override
+ public Void apply(TreeCacheListener listener)
+ {
+ try
+ {
+ listener.childEvent(client, event);
+ }
+ catch ( Exception e )
+ {
+ handleException(e);
+ }
+ return null;
+ }
+ });
}
/**
http://git-wip-us.apache.org/repos/asf/curator/blob/f4743336/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCacheEvent.java
----------------------------------------------------------------------
diff --git a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCacheEvent.java b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCacheEvent.java
index 2080d26..9548a14 100644
--- a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCacheEvent.java
+++ b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/cache/TreeCacheEvent.java
@@ -84,7 +84,15 @@ public class TreeCacheEvent
CONNECTION_LOST,
/**
- * Posted when the initial cache has been populated.
+ * Posted after the initial cache has been fully populated.
+ * <p/>
+ * On startup, the cache synchronizes its internal
+ * state with the server, publishing a series of {@link #NODE_ADDED} events as new nodes are discovered. Once
+ * the cachehas been fully synchronized, this {@link #INITIALIZED} this event is published. All events
+ * published after this event represent actual server-side mutations.
+ * <p/>
+ * Note: because the initial population is inherently asynchronous, so it's possible to observe server-side changes
+ * (such as a {@link #NODE_UPDATED}) prior to this event being published.
*/
INITIALIZED
}
http://git-wip-us.apache.org/repos/asf/curator/blob/f4743336/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/BaseTestTreeCache.java
----------------------------------------------------------------------
diff --git a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/BaseTestTreeCache.java b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/BaseTestTreeCache.java
new file mode 100644
index 0000000..f59af30
--- /dev/null
+++ b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/BaseTestTreeCache.java
@@ -0,0 +1,173 @@
+/**
+ * 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.curator.framework.recipes.cache;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.api.UnhandledErrorListener;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.test.BaseClassForTests;
+import org.apache.curator.test.Timing;
+import org.apache.curator.utils.CloseableUtils;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class BaseTestTreeCache extends BaseClassForTests
+{
+ private final Timing timing = new Timing();
+ CuratorFramework client;
+ TreeCache cache;
+ private List<Throwable> exceptions;
+ private BlockingQueue<TreeCacheEvent> events;
+ TreeCacheListener eventListener;
+
+ /**
+ * A TreeCache that records exceptions and automatically adds a listener.
+ */
+ class TreeCache extends org.apache.curator.framework.recipes.cache.TreeCache
+ {
+
+ TreeCache(CuratorFramework client, String path, boolean cacheData)
+ {
+ super(client, path, cacheData);
+ getListenable().addListener(eventListener);
+ }
+
+ @Override
+ protected void handleException(Throwable e)
+ {
+ exceptions.add(e);
+ }
+ }
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception
+ {
+ super.setup();
+
+ exceptions = new ArrayList<Throwable>();
+ events = new LinkedBlockingQueue<TreeCacheEvent>();
+ eventListener = new TreeCacheListener()
+ {
+ @Override
+ public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception
+ {
+ if ( event.getData() != null && event.getData().getPath().startsWith("/zookeeper") )
+ {
+ // Suppress any events related to /zookeeper paths
+ return;
+ }
+ events.add(event);
+ }
+ };
+
+ client = CuratorFrameworkFactory.newClient(server.getConnectString(), timing.session(), timing.connection(), new RetryOneTime(1));
+ client.start();
+ client.getUnhandledErrorListenable().addListener(new UnhandledErrorListener()
+ {
+ @Override
+ public void unhandledError(String message, Throwable e)
+ {
+ exceptions.add(e);
+ }
+ });
+ cache = new TreeCache(client, "/test", true);
+ }
+
+ @Override
+ @AfterMethod
+ public void teardown() throws Exception
+ {
+ try
+ {
+ try
+ {
+ if ( exceptions.size() == 1 )
+ {
+ Assert.fail("Exception was thrown", exceptions.get(0));
+ }
+ else if ( exceptions.size() > 1 )
+ {
+ AssertionError error = new AssertionError("Multiple exceptions were thrown");
+ for ( Throwable exception : exceptions )
+ {
+ error.addSuppressed(exception);
+ }
+ throw error;
+ }
+ }
+ finally
+ {
+ CloseableUtils.closeQuietly(cache);
+ CloseableUtils.closeQuietly(client);
+ }
+ }
+ finally
+ {
+ super.teardown();
+ }
+ }
+
+ void assertNoMoreEvents() throws InterruptedException
+ {
+ timing.sleepABit();
+ Assert.assertTrue(events.isEmpty());
+ }
+
+ TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType) throws InterruptedException
+ {
+ return assertEvent(expectedType, null);
+ }
+
+ TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType, String expectedPath) throws InterruptedException
+ {
+ return assertEvent(expectedType, expectedPath, null);
+ }
+
+ TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType, String expectedPath, byte[] expectedData) throws InterruptedException
+ {
+ TreeCacheEvent event = events.poll(timing.forWaiting().seconds(), TimeUnit.SECONDS);
+ Assert.assertNotNull(event, String.format("Expected type: %s, path: %s", expectedType, expectedPath));
+
+ String message = event.toString();
+ Assert.assertEquals(event.getType(), expectedType, message);
+ if ( expectedPath == null )
+ {
+ Assert.assertNull(event.getData(), message);
+ }
+ else
+ {
+ Assert.assertNotNull(event.getData(), message);
+ Assert.assertEquals(event.getData().getPath(), expectedPath, message);
+ }
+ if ( expectedData != null )
+ {
+ Assert.assertEquals(event.getData().getData(), expectedData, message);
+ }
+ return event;
+ }
+}
http://git-wip-us.apache.org/repos/asf/curator/blob/f4743336/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCache.java
----------------------------------------------------------------------
diff --git a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCache.java b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCache.java
index bc999bf..f35d24d 100644
--- a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCache.java
+++ b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCache.java
@@ -19,123 +19,17 @@
package org.apache.curator.framework.recipes.cache;
-import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ImmutableSet;
import org.apache.curator.framework.CuratorFramework;
-import org.apache.curator.framework.CuratorFrameworkFactory;
-import org.apache.curator.framework.api.UnhandledErrorListener;
-import org.apache.curator.retry.RetryOneTime;
-import org.apache.curator.test.BaseClassForTests;
import org.apache.curator.test.KillSession;
-import org.apache.curator.test.Timing;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-public class TestTreeCache extends BaseClassForTests
+public class TestTreeCache extends BaseTestTreeCache
{
- private final Timing timing = new Timing();
- private CuratorFramework client;
- private TreeCache cache;
- private List<Throwable> exceptions;
- private BlockingQueue<TreeCacheEvent> events;
- private TreeCacheListener eventListener;
-
- /**
- * A TreeCache that records exceptions.
- */
- class TreeCache extends org.apache.curator.framework.recipes.cache.TreeCache {
-
- TreeCache(CuratorFramework client, String path, boolean cacheData)
- {
- super(client, path, cacheData);
- }
-
- @Override
- protected void handleException(Throwable e)
- {
- exceptions.add(e);
- }
- }
-
- @Override
- @BeforeMethod
- public void setup() throws Exception
- {
- super.setup();
-
- exceptions = new ArrayList<Throwable>();
- events = new LinkedBlockingQueue<TreeCacheEvent>();
- eventListener = new TreeCacheListener()
- {
- @Override
- public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception
- {
- if (event.getData() != null && event.getData().getPath().startsWith("/zookeeper"))
- {
- // Suppress any events related to /zookeeper paths
- return;
- }
- events.add(event);
- }
- };
-
- client = CuratorFrameworkFactory.newClient(server.getConnectString(), timing.session(), timing.connection(), new RetryOneTime(1));
- client.start();
- client.getUnhandledErrorListenable().addListener(new UnhandledErrorListener()
- {
- @Override
- public void unhandledError(String message, Throwable e)
- {
- exceptions.add(e);
- }
- });
- cache = new TreeCache(client, "/test", true);
- cache.getListenable().addListener(eventListener);
- }
-
- @Override
- @AfterMethod
- public void teardown() throws Exception
- {
- try
- {
- try
- {
- if ( exceptions.size() == 1 )
- {
- Assert.fail("Exception was thrown", exceptions.get(0));
- }
- else if ( exceptions.size() > 1 )
- {
- AssertionError error = new AssertionError("Multiple exceptions were thrown");
- for ( Throwable exception : exceptions )
- {
- error.addSuppressed(exception);
- }
- throw error;
- }
- }
- finally
- {
- CloseableUtils.closeQuietly(cache);
- CloseableUtils.closeQuietly(client);
- }
- }
- finally
- {
- super.teardown();
- }
- }
-
@Test
public void testStartup() throws Exception
{
@@ -154,9 +48,9 @@ public class TestTreeCache extends BaseClassForTests
assertEvent(TreeCacheEvent.Type.INITIALIZED);
assertNoMoreEvents();
- Assert.assertEquals(cache.getCurrentChildren("/test"), ImmutableSortedSet.of("1", "2", "3"));
- Assert.assertEquals(cache.getCurrentChildren("/test/1"), ImmutableSortedSet.of());
- Assert.assertEquals(cache.getCurrentChildren("/test/2"), ImmutableSortedSet.of("sub"));
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("1", "2", "3"));
+ Assert.assertEquals(cache.getCurrentChildren("/test/1").keySet(), ImmutableSet.of());
+ Assert.assertEquals(cache.getCurrentChildren("/test/2").keySet(), ImmutableSet.of("sub"));
Assert.assertNull(cache.getCurrentChildren("/test/non_exist"));
}
@@ -176,6 +70,7 @@ public class TestTreeCache extends BaseClassForTests
{
client.create().forPath("/test");
client.create().forPath("/test/one", "hey there".getBytes());
+
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/one");
@@ -188,14 +83,19 @@ public class TestTreeCache extends BaseClassForTests
{
client.create().forPath("/test");
client.create().forPath("/test/one", "hey there".getBytes());
+
cache = new TreeCache(client, "/", true);
- cache.getListenable().addListener(eventListener);
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/one");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
assertNoMoreEvents();
+
+ Assert.assertTrue(cache.getCurrentChildren("/").keySet().contains("test"));
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("one"));
+ Assert.assertEquals(cache.getCurrentChildren("/test/one").keySet(), ImmutableSet.of());
+ Assert.assertEquals(new String(cache.getCurrentData("/test/one").getData()), "hey there");
}
@Test
@@ -205,13 +105,17 @@ public class TestTreeCache extends BaseClassForTests
client.create().forPath("/outer/foo");
client.create().forPath("/outer/test");
client.create().forPath("/outer/test/one", "hey there".getBytes());
+
cache = new TreeCache(client.usingNamespace("outer"), "/test", true);
- cache.getListenable().addListener(eventListener);
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/one");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
assertNoMoreEvents();
+
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("one"));
+ Assert.assertEquals(cache.getCurrentChildren("/test/one").keySet(), ImmutableSet.of());
+ Assert.assertEquals(new String(cache.getCurrentData("/test/one").getData()), "hey there");
}
@Test
@@ -221,8 +125,8 @@ public class TestTreeCache extends BaseClassForTests
client.create().forPath("/outer/foo");
client.create().forPath("/outer/test");
client.create().forPath("/outer/test/one", "hey there".getBytes());
+
cache = new TreeCache(client.usingNamespace("outer"), "/", true);
- cache.getListenable().addListener(eventListener);
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/foo");
@@ -230,6 +134,11 @@ public class TestTreeCache extends BaseClassForTests
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/one");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
assertNoMoreEvents();
+ Assert.assertEquals(cache.getCurrentChildren("/").keySet(), ImmutableSet.of("foo", "test"));
+ Assert.assertEquals(cache.getCurrentChildren("/foo").keySet(), ImmutableSet.of());
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("one"));
+ Assert.assertEquals(cache.getCurrentChildren("/test/one").keySet(), ImmutableSet.of());
+ Assert.assertEquals(new String(cache.getCurrentData("/test/one").getData()), "hey there");
}
@Test
@@ -237,6 +146,7 @@ public class TestTreeCache extends BaseClassForTests
{
cache.start();
assertEvent(TreeCacheEvent.Type.INITIALIZED);
+
client.create().forPath("/test");
client.create().forPath("/test/one", "hey there".getBytes());
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
@@ -264,10 +174,9 @@ public class TestTreeCache extends BaseClassForTests
@Test
public void testUpdateWhenNotCachingData() throws Exception
{
- cache = new TreeCache(client, "/test", false);
- cache.getListenable().addListener(eventListener);
-
client.create().forPath("/test");
+
+ cache = new TreeCache(client, "/test", false);
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
@@ -285,8 +194,8 @@ public class TestTreeCache extends BaseClassForTests
{
client.create().forPath("/test");
client.create().forPath("/test/foo", "one".getBytes());
- cache.start();
+ cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/foo");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
@@ -299,38 +208,12 @@ public class TestTreeCache extends BaseClassForTests
assertNoMoreEvents();
}
- // see https://github.com/Netflix/curator/issues/27 - was caused by not comparing old->new data
- @Test
- public void testIssue27() throws Exception
- {
- client.create().forPath("/test");
- client.create().forPath("/test/a");
- client.create().forPath("/test/b");
- client.create().forPath("/test/c");
-
- client.getChildren().forPath("/test");
-
- cache.start();
- assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
- assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/a");
- assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/b");
- assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/c");
- assertEvent(TreeCacheEvent.Type.INITIALIZED);
-
- client.delete().forPath("/test/a");
- client.create().forPath("/test/a");
- assertEvent(TreeCacheEvent.Type.NODE_REMOVED, "/test/a");
- assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/a");
-
- assertNoMoreEvents();
- }
-
@Test
public void testKilledSession() throws Exception
{
client.create().forPath("/test");
- cache.start();
+ cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
@@ -352,24 +235,25 @@ public class TestTreeCache extends BaseClassForTests
public void testBasics() throws Exception
{
client.create().forPath("/test");
+
cache.start();
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test");
assertEvent(TreeCacheEvent.Type.INITIALIZED);
- Assert.assertEquals(cache.getCurrentChildren("/test"), ImmutableSortedSet.of());
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of());
client.create().forPath("/test/one", "hey there".getBytes());
assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/test/one");
- Assert.assertEquals(cache.getCurrentChildren("/test"), ImmutableSortedSet.of("one"));
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("one"));
Assert.assertEquals(new String(cache.getCurrentData("/test/one").getData()), "hey there");
client.setData().forPath("/test/one", "sup!".getBytes());
assertEvent(TreeCacheEvent.Type.NODE_UPDATED, "/test/one");
- Assert.assertEquals(cache.getCurrentChildren("/test"), ImmutableSortedSet.of("one"));
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of("one"));
Assert.assertEquals(new String(cache.getCurrentData("/test/one").getData()), "sup!");
client.delete().forPath("/test/one");
assertEvent(TreeCacheEvent.Type.NODE_REMOVED, "/test/one");
- Assert.assertEquals(cache.getCurrentChildren("/test"), ImmutableSortedSet.of());
+ Assert.assertEquals(cache.getCurrentChildren("/test").keySet(), ImmutableSet.of());
assertNoMoreEvents();
}
@@ -378,6 +262,7 @@ public class TestTreeCache extends BaseClassForTests
public void testBasicsOnTwoCaches() throws Exception
{
TreeCache cache2 = new TreeCache(client, "/test", true);
+ cache2.getListenable().removeListener(eventListener); // Don't listen on the second cache.
// Just ensures the same event count; enables test flow control on cache2.
final Semaphore semaphore = new Semaphore(0);
@@ -393,6 +278,7 @@ public class TestTreeCache extends BaseClassForTests
try
{
client.create().forPath("/test");
+
cache.start();
cache2.start();
@@ -446,39 +332,4 @@ public class TestTreeCache extends BaseClassForTests
client.delete().forPath("/test/one");
assertNoMoreEvents();
}
-
- private void assertNoMoreEvents() throws InterruptedException
- {
- timing.sleepABit();
- Assert.assertTrue(events.isEmpty());
- }
-
- private TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType) throws InterruptedException
- {
- return assertEvent(expectedType, null);
- }
-
- private TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType, String expectedPath) throws InterruptedException
- {
- return assertEvent(expectedType, expectedPath, null);
- }
-
- private TreeCacheEvent assertEvent(TreeCacheEvent.Type expectedType, String expectedPath, byte[] expectedData) throws InterruptedException
- {
- TreeCacheEvent event = events.poll(timing.forWaiting().seconds(), TimeUnit.SECONDS);
- Assert.assertEquals(event.getType(), expectedType, event.toString());
- if ( expectedPath == null )
- {
- Assert.assertNull(event.getData(), event.toString());
- }
- else
- {
- Assert.assertEquals(event.getData().getPath(), expectedPath, event.toString());
- }
- if ( expectedData != null )
- {
- Assert.assertEquals(event.getData().getData(), expectedData, event.toString());
- }
- return event;
- }
}
http://git-wip-us.apache.org/repos/asf/curator/blob/f4743336/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCacheRandomTree.java
----------------------------------------------------------------------
diff --git a/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCacheRandomTree.java b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCacheRandomTree.java
new file mode 100644
index 0000000..368b557
--- /dev/null
+++ b/curator-recipes/src/test/java/org/apache/curator/framework/recipes/cache/TestTreeCacheRandomTree.java
@@ -0,0 +1,199 @@
+/**
+ * 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.curator.framework.recipes.cache;
+
+import com.google.common.collect.Iterables;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.utils.ZKPaths;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+public class TestTreeCacheRandomTree extends BaseTestTreeCache
+{
+ /**
+ * A randomly generated source-of-truth node for {@link #testGiantRandomDeepTree()}
+ */
+ private static final class TestNode
+ {
+ String fullPath;
+ byte[] data;
+ Map<String, TestNode> children = new HashMap<String, TestNode>();
+
+ TestNode(String fullPath, byte[] data)
+ {
+ this.fullPath = fullPath;
+ this.data = data;
+ }
+ }
+
+ // These constants will produce a tree about 10 levels deep.
+ private static final int ITERATIONS = 1000;
+ private static final double DIVE_CHANCE = 0.9;
+
+ private final Random random = new Random();
+
+ /**
+ * Randomly construct a large tree of test data in memory, mirror it into ZK, and then use
+ * a TreeCache to follow the changes. At each step, assert that TreeCache matches our
+ * source-of-truth test data, and that we see exactly the set of events we expect to see.
+ */
+ @Test
+ public void testGiantRandomDeepTree() throws Exception
+ {
+ client.create().forPath("/tree", null);
+ CuratorFramework cl = client.usingNamespace("tree");
+ cache = new TreeCache(cl, "/", true);
+ cache.getListenable().addListener(eventListener);
+ cache.start();
+ assertEvent(TreeCacheEvent.Type.NODE_ADDED, "/");
+ assertEvent(TreeCacheEvent.Type.INITIALIZED);
+
+ TestNode root = new TestNode("/", null);
+ int maxDepth = 0;
+ int adds = 0;
+ int removals = 0;
+ int updates = 0;
+
+ for ( int i = 0; i < ITERATIONS; ++i )
+ {
+ // Select a node to update, randomly navigate down through the tree
+ int depth = 0;
+ TestNode last = null;
+ TestNode node = root;
+ while ( !node.children.isEmpty() && random.nextDouble() < DIVE_CHANCE )
+ {
+ // Go down a level in the tree. Select a random child for the next iteration.
+ last = node;
+ node = Iterables.get(node.children.values(), random.nextInt(node.children.size()));
+ ++depth;
+ }
+ maxDepth = Math.max(depth, maxDepth);
+
+ // Okay we found a node, let's do something interesting with it.
+ switch ( random.nextInt(3) )
+ {
+ case 0:
+ // Try a removal if we have no children and we're not the root node.
+ if ( node != root && node.children.isEmpty() )
+ {
+ // Delete myself from parent.
+ TestNode removed = last.children.remove(ZKPaths.getNodeFromPath(node.fullPath));
+ Assert.assertSame(node, removed);
+
+ // Delete from ZK
+ cl.delete().forPath(node.fullPath);
+
+ // TreeCache should see the delete.
+ assertEvent(TreeCacheEvent.Type.NODE_REMOVED, node.fullPath);
+ ++removals;
+ }
+ break;
+ case 1:
+ // Do an update.
+ byte[] newData = new byte[10];
+ random.nextBytes(newData);
+
+ if ( Arrays.equals(node.data, newData) )
+ {
+ // Randomly generated the same data! Very small chance, just skip.
+ continue;
+ }
+
+ // Update source-of-truth.
+ node.data = newData;
+
+ // Update in ZK.
+ cl.setData().forPath(node.fullPath, node.data);
+
+ // TreeCache should see the update.
+ assertEvent(TreeCacheEvent.Type.NODE_UPDATED, node.fullPath, node.data);
+
+ ++updates;
+ break;
+ case 2:
+ // Add a new child.
+ String name = Long.toHexString(random.nextLong());
+ if ( node.children.containsKey(name) )
+ {
+ // Randomly generated the same name! Very small chance, just skip.
+ continue;
+ }
+
+ // Add a new child to our test tree.
+ byte[] data = new byte[10];
+ random.nextBytes(data);
+ TestNode child = new TestNode(ZKPaths.makePath(node.fullPath, name), data);
+ node.children.put(name, child);
+
+ // Add to ZK.
+ cl.create().forPath(child.fullPath, child.data);
+
+ // TreeCache should see the add.
+ assertEvent(TreeCacheEvent.Type.NODE_ADDED, child.fullPath, child.data);
+
+ ++adds;
+ break;
+ }
+
+ // Each iteration, ensure the cached state matches our source-of-truth tree.
+ assertNodeEquals(cache.getCurrentData("/"), root);
+ assertTreeEquals(cache, root);
+ }
+
+ // Typical stats for this test: maxDepth: 10, adds: 349, removals: 198, updates: 320
+ // We get more adds than removals because removals only happen if we're at a leaf.
+ System.out.println(String.format("maxDepth: %s, adds: %s, removals: %s, updates: %s", maxDepth, adds, removals, updates));
+ assertNoMoreEvents();
+ }
+
+ /**
+ * Recursively assert that current children equal expected children.
+ */
+ private static void assertTreeEquals(TreeCache cache, TestNode expectedNode)
+ {
+ String path = expectedNode.fullPath;
+ Map<String, ChildData> cacheChildren = cache.getCurrentChildren(path);
+ Assert.assertNotNull(cacheChildren, path);
+ Assert.assertEquals(cacheChildren.keySet(), expectedNode.children.keySet(), path);
+
+ for ( Map.Entry<String, TestNode> entry : expectedNode.children.entrySet() )
+ {
+ String nodeName = entry.getKey();
+ ChildData childData = cacheChildren.get(nodeName);
+ TestNode expectedChild = entry.getValue();
+ assertNodeEquals(childData, expectedChild);
+ assertTreeEquals(cache, expectedChild);
+ }
+ }
+
+ /**
+ * Assert that the given node data matches expected test node data.
+ */
+ private static void assertNodeEquals(ChildData actualChild, TestNode expectedNode)
+ {
+ String path = expectedNode.fullPath;
+ Assert.assertNotNull(actualChild, path);
+ Assert.assertEquals(actualChild.getData(), expectedNode.data, path);
+ }
+}