This is an automated email from the ASF dual-hosted git repository.
ddanielr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/main by this push:
new 3f81bc32e6 Caches non-existence of ZK node for ZooCache.getChildren
(#5143)
3f81bc32e6 is described below
commit 3f81bc32e684382fa0e36440ec24db422c4056a9
Author: Keith Turner <[email protected]>
AuthorDate: Mon Dec 9 07:31:06 2024 -0500
Caches non-existence of ZK node for ZooCache.getChildren (#5143)
* Caches non-existence of ZK node for ZooCache.getChildren
For a nodes data in zookeeper ZooCache was caching when a node did not
exist. However for getChildren when a node did not exist it would not
cache this and would always go to zookeeper. Also ZooCache was handling
a null return from Zookeeper.getChildren that would never happen. Fixed
these issues.
fixes #5047
* expire cache entries and remove their watches
* Update
test/src/main/java/org/apache/accumulo/test/zookeeper/ZooCacheIT.java
Co-authored-by: Christopher Tubbs <[email protected]>
* code review update
* added test for watch removal and fixed bug with watch removal
* Removes logging added for testing
---------
Co-authored-by: Christopher Tubbs <[email protected]>
---
.../accumulo/core/fate/zookeeper/ZcNode.java | 142 +++++++++++++++
.../accumulo/core/fate/zookeeper/ZooCache.java | 158 ++++++++---------
.../apache/accumulo/core/util/cache/Caches.java | 3 +-
.../accumulo/core/fate/zookeeper/ZooCacheTest.java | 125 ++++++++++++-
.../apache/accumulo/test/zookeeper/ZooCacheIT.java | 195 +++++++++++++++++++++
5 files changed, 532 insertions(+), 91 deletions(-)
diff --git
a/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZcNode.java
b/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZcNode.java
new file mode 100644
index 0000000000..bcd2b93872
--- /dev/null
+++ b/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZcNode.java
@@ -0,0 +1,142 @@
+/*
+ * 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
+ *
+ * https://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.accumulo.core.fate.zookeeper;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Immutable data class used by zoo cache to hold what it is caching for
single zookeeper node. Data
+ * and children are obtained from zookeeper at different times. This class is
structured so that
+ * data can be obtained first and then children added later or visa veras.
+ *
+ * <p>
+ * Four distinct states can be cached for a zookeeper node.
+ * <ul>
+ * <li>Can cache that a node does not exist in zookeeper. This state is
represented by data, state,
+ * and children all being null.</li>
+ * <li>Can cache only the data for a zookeeper node. For this state data and
stat are non-null while
+ * children is null. Calling getChildren on node in this state will throw an
exception.</li>
+ * <li>Can cache only the children for a zookeeper node. For this state
children is non-null while
+ * data and stat are null. Calling getData or getStat on node in this state
will throw an
+ * exception.</li>
+ * <li>Can cache the children and data for a zookeeper node. For this state
data,stat, and children
+ * are non-null.</li>
+ * </ul>
+ * <p>
+ *
+ */
+class ZcNode {
+
+ private final byte[] data;
+ private final ZooCache.ZcStat stat;
+ private final List<String> children;
+
+ static final ZcNode NON_EXISTENT = new ZcNode();
+
+ private ZcNode() {
+ this.data = null;
+ this.stat = null;
+ this.children = null;
+ }
+
+ /**
+ * Creates a new ZcNode that combines the data and stat from an existing
ZcNode and sets the
+ * children.
+ */
+ ZcNode(List<String> children, ZcNode existing) {
+ Objects.requireNonNull(children);
+ if (existing == null) {
+ this.data = null;
+ this.stat = null;
+ } else {
+ this.data = existing.data;
+ this.stat = existing.stat;
+ }
+
+ this.children = List.copyOf(children);
+ }
+
+ /**
+ * Creates a new ZcNode that combines the children from an existing ZcNode
and sets the data and
+ * stat.
+ */
+ ZcNode(byte[] data, ZooCache.ZcStat zstat, ZcNode existing) {
+ this.data = Objects.requireNonNull(data);
+ this.stat = Objects.requireNonNull(zstat);
+ if (existing == null) {
+ this.children = null;
+ } else {
+ this.children = existing.children;
+ }
+ }
+
+ /**
+ * @return the data if the node exists and the data was set OR return null
when the node does not
+ * exist
+ * @throws IllegalStateException in the case where the node exists and the
data was never set
+ */
+ byte[] getData() {
+ Preconditions.checkState(cachedData());
+ return data;
+ }
+
+ /**
+ * @return the stat if the node exists and the stat was set OR return null
when the node does not
+ * exist
+ * @throws IllegalStateException in the case where the node exists and the
data was never set
+ */
+ ZooCache.ZcStat getStat() {
+ Preconditions.checkState(cachedData());
+ return stat;
+ }
+
+ /**
+ * @return the children if the node exists and the children were set OR
return null when the node
+ * does not exist exists
+ * @throws IllegalStateException in the case where the node exists and the
children were never set
+ */
+ List<String> getChildren() {
+ Preconditions.checkState(cachedChildren());
+ return children;
+ }
+
+ /**
+ * @return true if the node does not exists or it exists and children are
cached.
+ */
+ boolean cachedChildren() {
+ return children != null || notExists();
+ }
+
+ /**
+ * @return true if the node does not exists or it exists and data and stat
cached.
+ */
+ boolean cachedData() {
+ return data != null || notExists();
+ }
+
+ /**
+ * @return true if the node does not exists in zookeeper
+ */
+ boolean notExists() {
+ return stat == null && data == null && children == null;
+ }
+}
diff --git
a/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZooCache.java
b/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZooCache.java
index 86b869fa15..ae8e752b20 100644
--- a/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZooCache.java
+++ b/core/src/main/java/org/apache/accumulo/core/fate/zookeeper/ZooCache.java
@@ -21,10 +21,11 @@ package org.apache.accumulo.core.fate.zookeeper;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.accumulo.core.util.LazySingletons.RANDOM;
+import java.time.Duration;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Predicate;
@@ -32,6 +33,7 @@ import java.util.function.Predicate;
import org.apache.accumulo.core.lock.ServiceLock;
import org.apache.accumulo.core.lock.ServiceLockData;
import org.apache.accumulo.core.lock.ServiceLockPaths.ServiceLockPath;
+import org.apache.accumulo.core.util.cache.Caches;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.WatchedEvent;
@@ -41,6 +43,8 @@ import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.RemovalListener;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -56,49 +60,10 @@ public class ZooCache {
private static final AtomicLong nextCacheId = new AtomicLong(0);
private final String cacheId = "ZC" + nextCacheId.incrementAndGet();
- private static class ZcNode {
- final byte[] data;
- final ZcStat stat;
- final boolean dataSet;
- final List<String> children;
- final boolean childrenSet;
-
- private ZcNode(ZcNode other, List<String> children) {
- this.data = other != null ? other.data : null;
- this.stat = other != null ? other.stat : null;
- this.dataSet = other != null ? other.dataSet : false;
- this.children = children;
- this.childrenSet = true;
- }
-
- public ZcNode(byte[] data, ZcStat zstat, ZcNode zcn) {
- this.data = data;
- this.stat = zstat;
- this.dataSet = true;
- this.children = zcn != null ? zcn.children : null;
- this.childrenSet = zcn != null ? zcn.childrenSet : false;
- }
-
- byte[] getData() {
- Preconditions.checkState(dataSet);
- return data;
- }
-
- ZcStat getStat() {
- Preconditions.checkState(dataSet);
- return stat;
- }
-
- List<String> getChildren() {
- Preconditions.checkState(childrenSet);
- return children;
- }
- }
-
- // ConcurrentHashMap will only allow one thread to run at a time for a given
key and this
- // implementation relies on that. Not all concurrent map implementations
have this behavior for
+ // The concurrent map returned by Caffiene will only allow one thread to run
at a time for a given
+ // key and ZooCache relies on that. Not all concurrent map implementations
have this behavior for
// their compute functions.
- private final ConcurrentHashMap<String,ZcNode> nodeCache;
+ private final ConcurrentMap<String,ZcNode> nodeCache;
private final ZooReader zReader;
@@ -161,6 +126,8 @@ public class ZooCache {
case NodeChildrenChanged:
case NodeCreated:
case NodeDeleted:
+ case ChildWatchRemoved:
+ case DataWatchRemoved:
remove(event.getPath());
break;
case None:
@@ -206,9 +173,30 @@ public class ZooCache {
* @param watcher watcher object
*/
public ZooCache(ZooReader reader, Watcher watcher) {
+ this(reader, watcher, Duration.ofMinutes(3));
+ }
+
+ public ZooCache(ZooReader reader, Watcher watcher, Duration timeout) {
this.zReader = reader;
- nodeCache = new ConcurrentHashMap<>();
this.externalWatcher = watcher;
+ RemovalListener<String,ZcNode> removalListerner = (path, zcNode, reason)
-> {
+ try {
+ log.trace("{} removing watches for {} because {}", cacheId, path,
reason);
+ reader.getZooKeeper().removeWatches(path, ZooCache.this.watcher,
Watcher.WatcherType.Any,
+ false);
+ } catch (InterruptedException | KeeperException | RuntimeException e) {
+ log.warn("{} failed to remove watches on path {} in zookeeper",
cacheId, path, e);
+ }
+ };
+ // Must register the removal listener using evictionListener inorder for
removal to be mutually
+ // exclusive with any other operations on the same path. This is important
for watcher
+ // consistency, concurrently adding and removing watches for the same path
would leave zoocache
+ // in a really bad state. The cache builder has another way to register a
removal listener that
+ // is not mutually exclusive.
+ Cache<String,ZcNode> cache =
+ Caches.getInstance().createNewBuilder(Caches.CacheName.ZOO_CACHE,
false)
+
.expireAfterAccess(timeout).evictionListener(removalListerner).build();
+ nodeCache = cache.asMap();
log.trace("{} created new cache", cacheId, new Exception());
}
@@ -316,43 +304,46 @@ public class ZooCache {
public List<String> run() throws KeeperException, InterruptedException {
var zcNode = nodeCache.get(zPath);
- if (zcNode != null && zcNode.childrenSet) {
+ if (zcNode != null && zcNode.cachedChildren()) {
return zcNode.getChildren();
}
log.trace("{} {} was not in children cache, looking up in zookeeper",
cacheId, zPath);
- try {
- zcNode = nodeCache.compute(zPath, (zp, zcn) -> {
- // recheck the children now that lock is held on key
- if (zcn != null && zcn.childrenSet) {
- return zcn;
- }
+ zcNode = nodeCache.compute(zPath, (zp, zcn) -> {
+ // recheck the children now that lock is held on key
+ if (zcn != null && zcn.cachedChildren()) {
+ return zcn;
+ }
- try {
- final ZooKeeper zooKeeper = getZooKeeper();
- List<String> children;
- children = zooKeeper.getChildren(zPath, watcher);
- if (children != null) {
- children = List.copyOf(children);
- }
- return new ZcNode(zcn, children);
- } catch (KeeperException e) {
- throw new ZcException(e);
- } catch (InterruptedException e) {
- throw new ZcInterruptedException(e);
+ try {
+ final ZooKeeper zooKeeper = getZooKeeper();
+ // Register a watcher on the node to monitor creation/deletion
events for the node. It
+ // is possible that an event from this watch could trigger prior
to calling getChildren.
+ // That is ok because the compute() call on the map has a lock and
processing the event
+ // will block until compute() returns. After compute() returns the
event processing
+ // would clear the map entry.
+ Stat stat = zooKeeper.exists(zPath, watcher);
+ if (stat == null) {
+ log.trace("{} getChildren saw that {} does not exists", cacheId,
zPath);
+ return ZcNode.NON_EXISTENT;
}
- });
- // increment this after compute call completes when the change is
visible
- updateCount.incrementAndGet();
- return zcNode.getChildren();
- } catch (ZcException zce) {
- if (zce.getZKException().code() == Code.NONODE) {
- return null;
- } else {
- throw zce;
+ List<String> children = zooKeeper.getChildren(zPath, watcher);
+ log.trace("{} adding {} children of {} to cache", cacheId,
children.size(), zPath);
+ return new ZcNode(children, zcn);
+ } catch (KeeperException.NoNodeException nne) {
+ log.trace("{} get children saw race condition for {}, node deleted
after exists call",
+ cacheId, zPath);
+ throw new ConcurrentModificationException(nne);
+ } catch (KeeperException e) {
+ throw new ZcException(e);
+ } catch (InterruptedException e) {
+ throw new ZcInterruptedException(e);
}
- }
+ });
+ // increment this after compute call completes when the change is
visible
+ updateCount.incrementAndGet();
+ return zcNode.getChildren();
}
};
@@ -386,7 +377,7 @@ public class ZooCache {
public byte[] run() throws KeeperException, InterruptedException {
var zcNode = nodeCache.get(zPath);
- if (zcNode != null && zcNode.dataSet) {
+ if (zcNode != null && zcNode.cachedData()) {
if (status != null) {
copyStats(status, zcNode.getStat());
}
@@ -398,7 +389,7 @@ public class ZooCache {
zcNode = nodeCache.compute(zPath, (zp, zcn) -> {
// recheck the now that lock is held on key, it may be present now.
Could have been
// computed while waiting for lock.
- if (zcn != null && zcn.dataSet) {
+ if (zcn != null && zcn.cachedData()) {
return zcn;
}
/*
@@ -412,18 +403,19 @@ public class ZooCache {
try {
final ZooKeeper zooKeeper = getZooKeeper();
Stat stat = zooKeeper.exists(zPath, watcher);
- byte[] data = null;
- ZcStat zstat = null;
if (stat == null) {
if (log.isTraceEnabled()) {
log.trace("{} zookeeper did not contain {}", cacheId, zPath);
}
+ return ZcNode.NON_EXISTENT;
} else {
+ byte[] data = null;
+ ZcStat zstat = null;
try {
data = zooKeeper.getData(zPath, watcher, stat);
zstat = new ZcStat(stat);
} catch (KeeperException.BadVersionException |
KeeperException.NoNodeException e1) {
- throw new ConcurrentModificationException();
+ throw new ConcurrentModificationException(e1);
} catch (InterruptedException e) {
throw new ZcInterruptedException(e);
}
@@ -431,8 +423,8 @@ public class ZooCache {
log.trace("{} zookeeper contained {} {}", cacheId, zPath,
(data == null ? null : new String(data, UTF_8)));
}
+ return new ZcNode(data, zstat, zcn);
}
- return new ZcNode(data, zstat, zcn);
} catch (KeeperException ke) {
throw new ZcException(ke);
} catch (InterruptedException e) {
@@ -502,9 +494,9 @@ public class ZooCache {
* @return true if data value is cached
*/
@VisibleForTesting
- boolean dataCached(String zPath) {
+ public boolean dataCached(String zPath) {
var zcn = nodeCache.get(zPath);
- return zcn != null && zcn.dataSet;
+ return zcn != null && zcn.cachedData();
}
/**
@@ -514,9 +506,9 @@ public class ZooCache {
* @return true if children are cached
*/
@VisibleForTesting
- boolean childrenCached(String zPath) {
+ public boolean childrenCached(String zPath) {
var zcn = nodeCache.get(zPath);
- return zcn != null && zcn.childrenSet;
+ return zcn != null && zcn.cachedChildren();
}
/**
diff --git a/core/src/main/java/org/apache/accumulo/core/util/cache/Caches.java
b/core/src/main/java/org/apache/accumulo/core/util/cache/Caches.java
index 67dc8f5bb5..8724f87e04 100644
--- a/core/src/main/java/org/apache/accumulo/core/util/cache/Caches.java
+++ b/core/src/main/java/org/apache/accumulo/core/util/cache/Caches.java
@@ -65,7 +65,8 @@ public class Caches implements MetricsProducer {
TSRM_FILE_LENGTHS,
TINYLFU_BLOCK_CACHE,
VOLUME_HDFS_CONFIGS,
- MINC_AGE
+ MINC_AGE,
+ ZOO_CACHE
}
private static final Logger LOG = LoggerFactory.getLogger(Caches.class);
diff --git
a/core/src/test/java/org/apache/accumulo/core/fate/zookeeper/ZooCacheTest.java
b/core/src/test/java/org/apache/accumulo/core/fate/zookeeper/ZooCacheTest.java
index 39a2184568..3aced2e103 100644
---
a/core/src/test/java/org/apache/accumulo/core/fate/zookeeper/ZooCacheTest.java
+++
b/core/src/test/java/org/apache/accumulo/core/fate/zookeeper/ZooCacheTest.java
@@ -176,6 +176,8 @@ public class ZooCacheTest {
@Test
public void testGetChildren() throws Exception {
+ Stat existsStat = new Stat();
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(CHILDREN);
replay(zk);
@@ -190,34 +192,61 @@ public class ZooCacheTest {
@Test
public void testGetChildren_NoKids() throws Exception {
- expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(null);
+ Stat existsStat = new Stat();
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
+ expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(List.of());
replay(zk);
- assertNull(zc.getChildren(ZPATH));
+ assertEquals(List.of(), zc.getChildren(ZPATH));
verify(zk);
- assertNull(zc.getChildren(ZPATH)); // cache hit
+ assertEquals(List.of(), zc.getChildren(ZPATH)); // cache hit
+ }
+
+ @Test
+ public void testGetChildren_RaceCondition() throws Exception {
+ // simulate the node being deleted between calling zookeeper.exists and
zookeeper.getChildren
+ Stat existsStat = new Stat();
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
+ expect(zk.getChildren(eq(ZPATH), anyObject(Watcher.class)))
+ .andThrow(new KeeperException.NoNodeException(ZPATH));
+ expect(zk.exists(eq(ZPATH), anyObject(Watcher.class))).andReturn(null);
+ replay(zk);
+ assertNull(zc.getChildren(ZPATH));
+ verify(zk);
+ assertNull(zc.getChildren(ZPATH));
}
@Test
public void testGetChildren_Retry() throws Exception {
+ Stat existsStat = new Stat();
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
expect(zk.getChildren(eq(ZPATH), anyObject(Watcher.class)))
.andThrow(new KeeperException.BadVersionException(ZPATH));
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(CHILDREN);
replay(zk);
assertEquals(CHILDREN, zc.getChildren(ZPATH));
verify(zk);
+ assertEquals(CHILDREN, zc.getChildren(ZPATH));
}
@Test
- public void testGetChildren_EatNoNode() throws Exception {
- expect(zk.getChildren(eq(ZPATH), anyObject(Watcher.class)))
- .andThrow(new KeeperException.NoNodeException(ZPATH));
+ public void testGetChildren_NoNode() throws Exception {
+ assertFalse(zc.childrenCached(ZPATH));
+ assertFalse(zc.dataCached(ZPATH));
+ expect(zk.exists(eq(ZPATH), anyObject(Watcher.class))).andReturn(null);
replay(zk);
assertNull(zc.getChildren(ZPATH));
verify(zk);
+ assertNull(zc.getChildren(ZPATH));
+ // when its discovered a node does not exists in getChildren then its also
known it does not
+ // exists for getData
+ assertNull(zc.get(ZPATH));
+ assertTrue(zc.childrenCached(ZPATH));
+ assertTrue(zc.dataCached(ZPATH));
}
private static class TestWatcher implements Watcher {
@@ -300,6 +329,82 @@ public class ZooCacheTest {
testWatchDataNode_Clear(Watcher.Event.KeeperState.Expired);
}
+ @Test
+ public void testGetDataThenChildren() throws Exception {
+ testGetBoth(true);
+ }
+
+ @Test
+ public void testGetChildrenThenDate() throws Exception {
+ testGetBoth(false);
+ }
+
+ private void testGetBoth(boolean getDataFirst) throws Exception {
+ assertFalse(zc.childrenCached(ZPATH));
+ assertFalse(zc.dataCached(ZPATH));
+
+ var uc1 = zc.getUpdateCount();
+
+ final long ephemeralOwner1 = 123456789L;
+ Stat existsStat1 = new Stat();
+ existsStat1.setEphemeralOwner(ephemeralOwner1);
+
+ final long ephemeralOwner2 = 987654321L;
+ Stat existsStat2 = new Stat();
+ existsStat2.setEphemeralOwner(ephemeralOwner2);
+
+ if (getDataFirst) {
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat1);
+ expect(zk.getData(eq(ZPATH), anyObject(Watcher.class),
eq(existsStat1))).andReturn(DATA);
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat2);
+ expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(CHILDREN);
+ } else {
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat2);
+ expect(zk.getChildren(eq(ZPATH),
anyObject(Watcher.class))).andReturn(CHILDREN);
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat1);
+ expect(zk.getData(eq(ZPATH), anyObject(Watcher.class),
eq(existsStat1))).andReturn(DATA);
+ }
+
+ replay(zk);
+
+ if (getDataFirst) {
+ var zcStat = new ZcStat();
+ var data = zc.get(ZPATH, zcStat);
+ assertEquals(ephemeralOwner1, zcStat.getEphemeralOwner());
+ assertArrayEquals(DATA, data);
+ } else {
+ var children = zc.getChildren(ZPATH);
+ assertEquals(CHILDREN, children);
+ }
+ var uc2 = zc.getUpdateCount();
+ assertTrue(uc1 < uc2);
+
+ if (getDataFirst) {
+ var children = zc.getChildren(ZPATH);
+ assertEquals(CHILDREN, children);
+ } else {
+ var zcStat = new ZcStat();
+ var data = zc.get(ZPATH, zcStat);
+ assertEquals(ephemeralOwner1, zcStat.getEphemeralOwner());
+ assertArrayEquals(DATA, data);
+ }
+ var uc3 = zc.getUpdateCount();
+ assertTrue(uc2 < uc3);
+
+ verify(zk);
+
+ var zcStat = new ZcStat();
+ var data = zc.get(ZPATH, zcStat);
+ // the stat is associated with the data so should aways see the one
returned by the call to get
+ // data and not get children
+ assertEquals(ephemeralOwner1, zcStat.getEphemeralOwner());
+ assertArrayEquals(DATA, data);
+ var children = zc.getChildren(ZPATH);
+ assertEquals(CHILDREN, children);
+ // everything is cached so the get calls on the cache should not change
the update count
+ assertEquals(uc3, zc.getUpdateCount());
+ }
+
private void testWatchDataNode_Clear(Watcher.Event.KeeperState state) throws
Exception {
WatchedEvent event = new WatchedEvent(Watcher.Event.EventType.None, state,
null);
TestWatcher exw = new TestWatcher(event);
@@ -347,7 +452,13 @@ public class ZooCacheTest {
private Watcher watchChildren(List<String> initialChildren) throws Exception
{
Capture<Watcher> cw = EasyMock.newCapture();
- expect(zk.getChildren(eq(ZPATH), capture(cw))).andReturn(initialChildren);
+ if (initialChildren == null) {
+ expect(zk.exists(eq(ZPATH), capture(cw))).andReturn(null);
+ } else {
+ Stat existsStat = new Stat();
+ expect(zk.exists(eq(ZPATH),
anyObject(Watcher.class))).andReturn(existsStat);
+ expect(zk.getChildren(eq(ZPATH),
capture(cw))).andReturn(initialChildren);
+ }
replay(zk);
zc.getChildren(ZPATH);
assertTrue(zc.childrenCached(ZPATH));
diff --git
a/test/src/main/java/org/apache/accumulo/test/zookeeper/ZooCacheIT.java
b/test/src/main/java/org/apache/accumulo/test/zookeeper/ZooCacheIT.java
new file mode 100644
index 0000000000..645b7c70d2
--- /dev/null
+++ b/test/src/main/java/org/apache/accumulo/test/zookeeper/ZooCacheIT.java
@@ -0,0 +1,195 @@
+/*
+ * 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
+ *
+ * https://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.accumulo.test.zookeeper;
+
+import static
org.apache.accumulo.harness.AccumuloITBase.ZOOKEEPER_TESTING_SERVER;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.accumulo.core.fate.zookeeper.ZooCache;
+import org.apache.accumulo.core.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.test.util.Wait;
+import org.apache.zookeeper.Watcher;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+@Tag(ZOOKEEPER_TESTING_SERVER)
+public class ZooCacheIT {
+
+ private ZooKeeperTestingServer szk = null;
+ private ZooReaderWriter zk = null;
+
+ @TempDir
+ private File tempDir;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ szk = new ZooKeeperTestingServer(tempDir);
+ zk = szk.getZooReaderWriter();
+ }
+
+ @AfterEach
+ public void teardown() throws Exception {
+ szk.close();
+ }
+
+ @Test
+ public void testGetChildren() throws Exception {
+
+ Set<String> watchesRemoved = Collections.synchronizedSet(new HashSet<>());
+ Watcher watcher = event -> {
+ if (event.getType() == Watcher.Event.EventType.ChildWatchRemoved
+ || event.getType() == Watcher.Event.EventType.DataWatchRemoved) {
+ watchesRemoved.add(event.getPath());
+ }
+ };
+ ZooCache zooCache = new ZooCache(zk, watcher, Duration.ofSeconds(3));
+
+ zk.mkdirs("/test2");
+ zk.mkdirs("/test3/c1");
+ zk.mkdirs("/test3/c2");
+
+ // cache non-existence of /test1 and existence of /test2 and /test3
+ long uc1 = zooCache.getUpdateCount();
+ assertNull(zooCache.getChildren("/test1"));
+ long uc2 = zooCache.getUpdateCount();
+ assertTrue(uc1 < uc2);
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ long uc3 = zooCache.getUpdateCount();
+ assertTrue(uc2 < uc3);
+ assertEquals(Set.of("c1", "c2"),
Set.copyOf(zooCache.getChildren("/test3")));
+ long uc4 = zooCache.getUpdateCount();
+ assertTrue(uc3 < uc4);
+
+ // The cache should be stable now and new accesses should not change the
update count
+ assertNull(zooCache.getChildren("/test1"));
+ // once getChildren discovers that a node does not exists, then get data
will also know this
+ assertNull(zooCache.get("/test1"));
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c2"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc4, zooCache.getUpdateCount());
+
+ // Had cached non-existence of "/test1", should get a notification that it
was created
+ zk.mkdirs("/test1");
+
+ Wait.waitFor(() -> {
+ var children = zooCache.getChildren("/test1");
+ return children != null && children.isEmpty();
+ });
+
+ long uc5 = zooCache.getUpdateCount();
+ assertTrue(uc4 < uc5);
+ assertEquals(List.of(), zooCache.getChildren("/test1"));
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c2"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc5, zooCache.getUpdateCount());
+
+ // add a child to /test3, should get a notification of the change
+ zk.mkdirs("/test3/c3");
+ Wait.waitFor(() -> {
+ var children = zooCache.getChildren("/test3");
+ return children != null && children.size() == 3;
+ });
+ long uc6 = zooCache.getUpdateCount();
+ assertTrue(uc5 < uc6);
+ assertEquals(List.of(), zooCache.getChildren("/test1"));
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c2", "c3"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc6, zooCache.getUpdateCount());
+
+ // remove a child from /test3
+ zk.delete("/test3/c2");
+ Wait.waitFor(() -> {
+ var children = zooCache.getChildren("/test3");
+ return children != null && children.size() == 2;
+ });
+ long uc7 = zooCache.getUpdateCount();
+ assertTrue(uc6 < uc7);
+ assertEquals(List.of(), zooCache.getChildren("/test1"));
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c3"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc7, zooCache.getUpdateCount());
+
+ // remove /test2, should start caching that it does not exist
+ zk.delete("/test2");
+ Wait.waitFor(() -> zooCache.getChildren("/test2") == null);
+ long uc8 = zooCache.getUpdateCount();
+ assertTrue(uc7 < uc8);
+ assertEquals(List.of(), zooCache.getChildren("/test1"));
+ assertNull(zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c3"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc8, zooCache.getUpdateCount());
+
+ // add /test2 back, should update
+ zk.mkdirs("/test2");
+ Wait.waitFor(() -> zooCache.getChildren("/test2") != null);
+ long uc9 = zooCache.getUpdateCount();
+ assertTrue(uc8 < uc9);
+ assertEquals(List.of(), zooCache.getChildren("/test1"));
+ assertEquals(List.of(), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c1", "c3"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc9, zooCache.getUpdateCount());
+
+ // make multiple changes. the cache should see all of these
+ zk.delete("/test1");
+ zk.mkdirs("/test2/ca");
+ zk.delete("/test3/c1");
+ zk.mkdirs("/test3/c4");
+ zk.delete("/test3/c4");
+ zk.mkdirs("/test3/c5");
+
+ Wait.waitFor(() -> {
+ var children1 = zooCache.getChildren("/test1");
+ var children2 = zooCache.getChildren("/test2");
+ var children3 = zooCache.getChildren("/test3");
+ return children1 == null && children2 != null && children2.size() == 1
&& children3 != null
+ && Set.copyOf(children3).equals(Set.of("c3", "c5"));
+ });
+ long uc10 = zooCache.getUpdateCount();
+ assertTrue(uc9 < uc10);
+ assertNull(zooCache.getChildren("/test1"));
+ assertEquals(List.of("ca"), zooCache.getChildren("/test2"));
+ assertEquals(Set.of("c3", "c5"),
Set.copyOf(zooCache.getChildren("/test3")));
+ assertEquals(uc10, zooCache.getUpdateCount());
+
+ // wait for the cache to evict and clear watches
+ Wait.waitFor(() -> {
+ // the cache will not run its eviction handler unless accessed, so
access something that is
+ // not expected to be evicted
+ zooCache.getChildren("/test4");
+ return watchesRemoved.equals(Set.of("/test1", "/test2", "/test3"));
+ });
+
+ assertFalse(zooCache.childrenCached("/test1"));
+ assertFalse(zooCache.childrenCached("/test2"));
+ assertFalse(zooCache.childrenCached("/test3"));
+ }
+}