├── .gitignore ├── DO_NOT_CI ├── LICENSE ├── OWNERS ├── README ├── README.markdown ├── pom.xml └── src ├── main └── scala │ └── com │ └── twitter │ └── zookeeper │ ├── ZooKeeperClient.scala │ └── ZookeeperClientConfig.scala └── test └── scala └── com └── twitter └── zookeeper ├── TestConfig.scala └── ZooKeeperClientSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | dist 3 | #*# 4 | .#* 5 | *.log 6 | *.iml 7 | *.ipr 8 | *.iws 9 | *.swp 10 | *~ 11 | lib_managed 12 | project/boot 13 | .DS_Store 14 | src_managed 15 | -------------------------------------------------------------------------------- /DO_NOT_CI: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-archive/scala-zookeeper-client/c8a4189815d5d922e67be38918f2191d74738a2d/DO_NOT_CI -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Alex Payne 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | ryan -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This library is deprecated. Instead use util-zk (if you want a purely async implementation) or the 2 | client from twitter commons. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # scala-zookeeper-client (DEPRECATED) 2 | 3 | **This project is deprecated and will not be maintained.** 4 | 5 | See util-zk as the replacement library: http://github.com/twitter/util 6 | 7 | ## About 8 | 9 | A Scala client for [Apache ZooKeeper](http://hadoop.apache.org/zookeeper/), "a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services". 10 | 11 | ZooKeeper provides a Java client library that's perfectly usable from Scala. This just wraps some idioms and niceties around that library to make it as Scala-friendly as possible. It also ships with tests, so you can have some confidence that you'll be able to interact with ZooKeeper from Scala in a predictable and reliable way. 12 | 13 | The scala-zookeeper-client will automatically handle session expired events by creating a new connection to the Zookeeper servers. We also provide several commonly used operations and utilities on top of Zookeeper's minimal API: 14 | 15 | * Create a path of nodes, similar to 'mkdir -p' 16 | * Recursively delete a tree of nodes 17 | * Watch a node forever 18 | * Monitor a node's child set 19 | * For a given node, maintain a map from the node's children to the each child's data 20 | 21 | ## Usage 22 | 23 | ### Basic usage: 24 | 25 | import com.twitter.zookeeper.ZooKeeperClient 26 | 27 | val zk = new ZooKeeperClient("localhost:2181") 28 | 29 | zk.createPath("/a/b/c") 30 | zk.createPath("/a/b/d") 31 | val children = zk.getChildren("/a/b") // Seq("c", "d") 32 | 33 | zk.set("/a/b/c", "foo".getBytes()) 34 | val s = new String(zk.get("/a/b/c")) // "foo" 35 | 36 | zk.deleteRecursive("/a") // delete "a" and all children 37 | 38 | ### Advanced features: 39 | 40 | Monitor a node forever: 41 | 42 | import com.twitter.zookeeper.ZooKeeperClient 43 | import org.apache.zookeeper.CreateMode 44 | 45 | val zk = new ZooKeeperClient("localhost:2181") 46 | zk.create("/test-node", "foo".getBytes, CreateMode.PERSISTENT) 47 | zk.watchNode("/test-node", { (data : Option[Array[Byte]]) => 48 | data match { 49 | case Some(d) => println("Data updated: %s".format(new String(d))) 50 | case None => println("Node deleted") 51 | } 52 | }) 53 | zk.set("/test-node", "bar".getBytes) 54 | zk.set("/test-node", "baz".getBytes) 55 | zk.delete("/test-node") 56 | 57 | Monitor a node's children: 58 | 59 | import com.twitter.zookeeper.ZooKeeperClient 60 | import org.apache.zookeeper.CreateMode 61 | 62 | val zk = new ZooKeeperClient("localhost:2181") 63 | zk.create("/parent", null, CreateMode.PERSISTENT) 64 | zk.watchChildren("/parent", { (children : Seq[String]) => 65 | println("Children: %s".format(children.mkString(", "))) 66 | }) 67 | zk.create("/parent/child1", null, CreateMode.PERSISTENT) 68 | zk.create("/parent/child2", null, CreateMode.PERSISTENT) 69 | zk.delete("/parent/child1") 70 | zk.create("/parent/child3", null, CreateMode.PERSISTENT) 71 | zk.deleteRecursive("/parent") 72 | 73 | For a given node, automatically maintain a map from the node's children to the each child's data: 74 | 75 | import com.twitter.zookeeper.ZooKeeperClient 76 | import org.apache.zookeeper.CreateMode 77 | import scala.collection.mutable 78 | 79 | val zk = new ZooKeeperClient("localhost:2181") 80 | val childMap = mutable.Map[String, String]() 81 | 82 | zk.create("/parent", null, CreateMode.PERSISTENT) 83 | zk.watchChildrenWithData("/parent", childMap, {data => new String(data)}) 84 | 85 | zk.create("/parent/a", "foo".getBytes, CreateMode.PERSISTENT) 86 | zk.create("/parent/b", "bar".getBytes, CreateMode.PERSISTENT) 87 | println("child map: %s".format(childMap)) // NOTE: real code should synchronize access on childMap 88 | 89 | zk.delete("/parent/a") 90 | zk.set("/parent/b", "bar2".getBytes) 91 | zk.create("/parent/c", "baz".getBytes, CreateMode.PERSISTENT) 92 | println("child map: %s".format(childMap)) // NOTE: real code should synchronize access on childMap 93 | 94 | Get the internal zookeeper handle to pass to other libraries: 95 | 96 | import com.twitter.zookeeper.ZooKeeperClient 97 | import org.apache.zookeeper.ZooKeeper 98 | 99 | def zkListener(zkHandle : ZooKeeper) { 100 | // use zkHandle 101 | println("Internal zookeeper handle changed: %s".format(zkHandle)) 102 | // Note that zkListener will be called with a new handle whenever the Zookeeper session 103 | // has expired and a new connection to the Zookeeper servers is created 104 | } 105 | 106 | val zk = new ZooKeeperClient("localhost:2181", zkListener _) 107 | 108 | ## Authors 109 | 110 | * [John Corwin](http://github.com/jcorwin) 111 | * [Steve Jenson](http://github.com/stevej) 112 | * [Matt Knox](http://github.com/mattknox) 113 | * [Ian Ownbey](http://github.com/imownbey) 114 | * [Ryan King](http://github.com/ryanking) 115 | * [Alex Payne](http://github.com/al3x) 116 | 117 | ## License 118 | 119 | Apache License, Version 2.0. See the LICENSE file for more information. 120 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.twitter 5 | scala-zookeeper-client 6 | jar 7 | 3.0.8-SNAPSHOT 8 | 9 | com.twitter 10 | scala-parent-292 11 | 0.0.4 12 | ../../parents/scala-parent-292/pom.xml 13 | 14 | 15 | ${project.basedir}/../.git 16 | 17 | 18 | 19 | 20 | org.apache.zookeeper 21 | zookeeper 22 | 3.4.3 23 | 24 | 25 | 26 | com.sun.jdmk 27 | jmxtools 28 | 29 | 30 | com.sun.jmx 31 | jmxri 32 | 33 | 34 | javax.jms 35 | jms 36 | 37 | 38 | 39 | 40 | 41 | com.twitter 42 | ostrich 43 | 8.2.0 44 | 45 | 46 | com.twitter 47 | util-logging 48 | 5.3.0 49 | 50 | 51 | 52 | 53 | 54 | com.twitter 55 | maven-finagle-thrift-plugin 56 | 57 | 58 | finagle 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/zookeeper/ZooKeeperClient.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.zookeeper 2 | 3 | import scala.collection.JavaConversions._ 4 | import scala.collection.mutable 5 | import scala.collection.immutable.Set 6 | import org.apache.zookeeper.{CreateMode, KeeperException, Watcher, WatchedEvent, ZooKeeper} 7 | import org.apache.zookeeper.data.{ACL, Stat, Id} 8 | import org.apache.zookeeper.ZooDefs.Ids 9 | import org.apache.zookeeper.Watcher.Event.EventType 10 | import org.apache.zookeeper.Watcher.Event.KeeperState 11 | import com.twitter.logging.Logger 12 | import java.util.concurrent.{CountDownLatch, TimeUnit} 13 | import java.util.concurrent.atomic.AtomicBoolean 14 | 15 | @deprecated("scala-zookeeper-client is deprecated in favor of util-zk", "3.0.7") 16 | class ZooKeeperClient(servers: String, sessionTimeout: Int, basePath : String, 17 | watcher: Option[ZooKeeperClient => Unit]) { 18 | private val log = Logger.get 19 | @volatile private var zk : ZooKeeper = null 20 | connect() 21 | 22 | def this(servers: String, sessionTimeout: Int, basePath : String) = 23 | this(servers, sessionTimeout, basePath, None) 24 | 25 | def this(servers: String, sessionTimeout: Int, basePath : String, watcher: ZooKeeperClient => Unit) = 26 | this(servers, sessionTimeout, basePath, Some(watcher)) 27 | 28 | def this(servers: String) = 29 | this(servers, 3000, "", None) 30 | 31 | def this(servers: String, watcher: ZooKeeperClient => Unit) = 32 | this(servers, 3000, "", Some(watcher)) 33 | 34 | def this(config: ZookeeperClientConfig, watcher: Option[ZooKeeperClient => Unit]) = { 35 | this(config.hostList, 36 | config.sessionTimeout, 37 | config.basePath, 38 | watcher) 39 | } 40 | 41 | def this(config: ZookeeperClientConfig) ={ 42 | this(config, None) 43 | } 44 | 45 | def getHandle() : ZooKeeper = zk 46 | 47 | /** 48 | * connect() attaches to the remote zookeeper and sets an instance variable. 49 | */ 50 | private def connect() { 51 | val connectionLatch = new CountDownLatch(1) 52 | val assignLatch = new CountDownLatch(1) 53 | if (zk != null) { 54 | zk.close() 55 | zk = null 56 | } 57 | zk = new ZooKeeper(servers, sessionTimeout, 58 | new Watcher { def process(event : WatchedEvent) { 59 | sessionEvent(assignLatch, connectionLatch, event) 60 | }}) 61 | assignLatch.countDown() 62 | log.info("Attempting to connect to zookeeper servers %s", servers) 63 | connectionLatch.await() 64 | } 65 | 66 | def sessionEvent(assignLatch: CountDownLatch, connectionLatch : CountDownLatch, event : WatchedEvent) { 67 | log.info("Zookeeper event: %s".format(event)) 68 | assignLatch.await() 69 | event.getState match { 70 | case KeeperState.SyncConnected => { 71 | try { 72 | watcher.map(fn => fn(this)) 73 | } catch { 74 | case e:Exception => 75 | log.error(e, "Exception during zookeeper connection established callback") 76 | } 77 | connectionLatch.countDown() 78 | } 79 | case KeeperState.Expired => { 80 | // Session was expired; create a new zookeeper connection 81 | connect() 82 | } 83 | case _ => // Disconnected -- zookeeper library will handle reconnects 84 | } 85 | } 86 | 87 | /** 88 | * Given a string representing a path, return each subpath 89 | * Ex. subPaths("/a/b/c", "/") == ["/a", "/a/b", "/a/b/c"] 90 | */ 91 | def subPaths(path : String, sep : Char) = { 92 | val l = path.split(sep).toList 93 | val paths = l.tail.foldLeft[List[String]](Nil){(xs, x) => 94 | (xs.headOption.getOrElse("") + sep.toString + x)::xs} 95 | paths.reverse 96 | } 97 | 98 | private def makeNodePath(path : String) = "%s/%s".format(basePath, path).replaceAll("//", "/") 99 | 100 | def getChildren(path: String): Seq[String] = { 101 | zk.getChildren(makeNodePath(path), false) 102 | } 103 | 104 | def close() = zk.close 105 | 106 | def isAlive: Boolean = { 107 | // If you can get the root, then we're alive. 108 | val result: Stat = zk.exists("/", false) // do not watch 109 | result.getVersion >= 0 110 | } 111 | 112 | def create(path: String, data: Array[Byte], createMode: CreateMode): String = { 113 | zk.create(makeNodePath(path), data, Ids.OPEN_ACL_UNSAFE, createMode) 114 | } 115 | 116 | /** 117 | * ZooKeeper version of mkdir -p 118 | */ 119 | def createPath(path: String) { 120 | for (path <- subPaths(makeNodePath(path), '/')) { 121 | try { 122 | log.debug("Creating path in createPath: %s", path) 123 | zk.create(path, null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT) 124 | } catch { 125 | case _:KeeperException.NodeExistsException => {} // ignore existing nodes 126 | } 127 | } 128 | } 129 | 130 | def get(path: String): Array[Byte] = { 131 | zk.getData(makeNodePath(path), false, null) 132 | } 133 | 134 | def set(path: String, data: Array[Byte]) { 135 | zk.setData(makeNodePath(path), data, -1) 136 | } 137 | 138 | def delete(path: String) { 139 | zk.delete(makeNodePath(path), -1) 140 | } 141 | 142 | /** 143 | * Delete a node along with all of its children 144 | */ 145 | def deleteRecursive(path : String) { 146 | val children = getChildren(path) 147 | for (node <- children) { 148 | deleteRecursive(path + '/' + node) 149 | } 150 | delete(path) 151 | } 152 | 153 | /** 154 | * Watches a node. When the node's data is changed, onDataChanged will be called with the 155 | * new data value as a byte array. If the node is deleted, onDataChanged will be called with 156 | * None and will track the node's re-creation with an existence watch. 157 | */ 158 | def watchNode(node : String, onDataChanged : Option[Array[Byte]] => Unit) { 159 | log.debug("Watching node %s", node) 160 | val path = makeNodePath(node) 161 | def updateData { 162 | try { 163 | onDataChanged(Some(zk.getData(path, dataGetter, null))) 164 | } catch { 165 | case e:KeeperException => { 166 | log.warning("Failed to read node %s: %s", path, e) 167 | deletedData 168 | } 169 | } 170 | } 171 | 172 | def deletedData { 173 | onDataChanged(None) 174 | if (zk.exists(path, dataGetter) != null) { 175 | // Node was re-created by the time we called zk.exist 176 | updateData 177 | } 178 | } 179 | def dataGetter = new Watcher { 180 | def process(event : WatchedEvent) { 181 | if (event.getType == EventType.NodeDataChanged || event.getType == EventType.NodeCreated) { 182 | updateData 183 | } else if (event.getType == EventType.NodeDeleted) { 184 | deletedData 185 | } 186 | } 187 | } 188 | updateData 189 | } 190 | 191 | /** 192 | * Gets the children for a node (relative path from our basePath), watches 193 | * for each NodeChildrenChanged event and runs the supplied updateChildren function and 194 | * re-watches the node's children. 195 | */ 196 | def watchChildren(node : String, updateChildren : Seq[String] => Unit) { 197 | val path = makeNodePath(node) 198 | val childWatcher = new Watcher { 199 | def process(event : WatchedEvent) { 200 | if (event.getType == EventType.NodeChildrenChanged || 201 | event.getType == EventType.NodeCreated) { 202 | watchChildren(node, updateChildren) 203 | } 204 | } 205 | } 206 | try { 207 | val children = zk.getChildren(path, childWatcher) 208 | updateChildren(children) 209 | } catch { 210 | case e:KeeperException => { 211 | // Node was deleted -- fire a watch on node re-creation 212 | log.warning("Failed to read node %s: %s", path, e) 213 | updateChildren(List()) 214 | zk.exists(path, childWatcher) 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * WARNING: watchMap must be thread-safe. Writing is synchronized on the watchMap. Readers MUST 221 | * also synchronize on the watchMap for safety. 222 | */ 223 | def watchChildrenWithData[T](node : String, watchMap: mutable.Map[String, T], deserialize: Array[Byte] => T) { 224 | watchChildrenWithData(node, watchMap, deserialize, None) 225 | } 226 | 227 | /** 228 | * Watch a set of nodes with an explicit notifier. The notifier will be called whenever 229 | * the watchMap is modified 230 | */ 231 | def watchChildrenWithData[T](node : String, watchMap: mutable.Map[String, T], 232 | deserialize: Array[Byte] => T, notifier: String => Unit) { 233 | watchChildrenWithData(node, watchMap, deserialize, Some(notifier)) 234 | } 235 | 236 | private def watchChildrenWithData[T](node : String, watchMap: mutable.Map[String, T], 237 | deserialize: Array[Byte] => T, notifier: Option[String => Unit]) { 238 | def nodeChanged(child : String)(childData : Option[Array[Byte]]) { 239 | childData match { 240 | case Some(data) => { 241 | watchMap.synchronized { 242 | watchMap(child) = deserialize(data) 243 | } 244 | notifier.map(f => f(child)) 245 | } 246 | case None => // deletion handled via parent watch 247 | } 248 | } 249 | 250 | def parentWatcher(children : Seq[String]) { 251 | val childrenSet = Set(children : _*) 252 | val watchedKeys = Set(watchMap.keySet.toSeq : _*) 253 | val removedChildren = watchedKeys -- childrenSet 254 | val addedChildren = childrenSet -- watchedKeys 255 | watchMap.synchronized { 256 | // remove deleted children from the watch map 257 | for (child <- removedChildren) { 258 | log.ifDebug {"Node %s: child %s removed".format(node, child)} 259 | watchMap -= child 260 | } 261 | // add new children to the watch map 262 | for (child <- addedChildren) { 263 | // node is added via nodeChanged callback 264 | log.ifDebug {"Node %s: child %s added".format(node, child)} 265 | watchNode("%s/%s".format(node, child), nodeChanged(child)) 266 | } 267 | } 268 | for (child <- removedChildren) { 269 | notifier.map(f => f(child)) 270 | } 271 | } 272 | 273 | watchChildren(node, parentWatcher) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/zookeeper/ZookeeperClientConfig.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.zookeeper 2 | 3 | import com.twitter.ostrich.admin.RuntimeEnvironment 4 | import com.twitter.util.Config 5 | 6 | @deprecated("scala-zookeeper-client is deprecated in favor of util-zk", "3.0.7") 7 | trait ZookeeperClientConfig extends Config[ZooKeeperClient] { 8 | var hostList = required[String] 9 | var sessionTimeout = 3000 10 | var basePath = "" 11 | 12 | def apply = { 13 | new ZooKeeperClient(this) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/zookeeper/TestConfig.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.zookeeper 2 | 3 | class TestConfig extends ZookeeperClientConfig { 4 | hostList = "localhost:2181" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/zookeeper/ZooKeeperClientSpec.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.zookeeper 2 | 3 | import java.net.{Socket, ConnectException} 4 | import org.apache.zookeeper.{CreateMode, Watcher, WatchedEvent, ZooKeeper} 5 | import org.apache.zookeeper.CreateMode._ 6 | import org.apache.zookeeper.KeeperException.NoNodeException 7 | import org.apache.zookeeper.data.{ACL, Id} 8 | import org.specs._ 9 | import scala.collection.mutable 10 | 11 | class ZookeeperClientSpec extends SpecificationWithJUnit { 12 | val config = new TestConfig 13 | val hostlist = config.hostList 14 | // 15 | // doBeforeSpec { 16 | // // we need to be sure that a ZooKeeper server is running in order to test 17 | // println("Testing connection to ZooKeeper server at %s...".format(hostlist)) 18 | // val socketPort = hostlist.split(":") 19 | // new Socket(socketPort(0), socketPort(1).toInt) mustNot throwA[ConnectException] 20 | // } 21 | // 22 | // "ZookeeperClient" should { 23 | // shareVariables() 24 | // var zkClient : ZooKeeperClient = null 25 | // 26 | // doFirst { 27 | // println("Attempting to connect to ZooKeeper server %s...".format(hostlist)) 28 | // zkClient = new ZooKeeperClient(config, None) 29 | // } 30 | // 31 | // doLast { 32 | // zkClient.close 33 | // } 34 | // 35 | // "be able to be instantiated with a FakeWatcher" in { 36 | // zkClient mustNot beNull 37 | // } 38 | // 39 | // "connect to local Zookeeper server and retrieve version" in { 40 | // zkClient.isAlive mustBe true 41 | // } 42 | // 43 | // "get data at a known-good specified path" in { 44 | // val results: Array[Byte] = zkClient.get("/") 45 | // results.size must beGreaterThanOrEqualTo(0) 46 | // } 47 | // 48 | // "get data at a known-bad specified path" in { 49 | // zkClient.get("/thisdoesnotexist") must throwA[NoNodeException] 50 | // } 51 | // 52 | // "get list of children" in { 53 | // zkClient.getChildren("/") must notBeEmpty 54 | // } 55 | // 56 | // "create a node at a specified path" in { 57 | // val data: Array[Byte] = Array(0x63) 58 | // val id = new Id("world", "anyone") 59 | // val createMode = EPHEMERAL 60 | // 61 | // zkClient.create("/foo", data, createMode) mustEqual "/foo" 62 | // zkClient.delete("/foo") 63 | // } 64 | // 65 | // "watch a node" in { 66 | // val data: Array[Byte] = Array(0x63) 67 | // val node = "/datanode" 68 | // val createMode = EPHEMERAL 69 | // var watchCount = 0 70 | // def watcher(data : Option[Array[Byte]]) { 71 | // watchCount += 1 72 | // } 73 | // zkClient.create(node, data, createMode) 74 | // zkClient.watchNode(node, watcher) 75 | // Thread.sleep(50L) 76 | // watchCount mustEqual 1 77 | // zkClient.delete("/datanode") 78 | // } 79 | // 80 | // "watch a tree of nodes" in { 81 | // var children : Seq[String] = List() 82 | // var watchCount = 0 83 | // def watcher(nodes : Seq[String]) { 84 | // watchCount += 1 85 | // children = nodes 86 | // } 87 | // zkClient.createPath("/tree/a") 88 | // zkClient.createPath("/tree/b") 89 | // zkClient.watchChildren("/tree", watcher) 90 | // children.size mustEqual 2 91 | // children must containAll(List("a", "b")) 92 | // watchCount mustEqual 1 93 | // zkClient.createPath("/tree/c") 94 | // Thread.sleep(50L) 95 | // children.size mustEqual 3 96 | // children must containAll(List("a", "b", "c")) 97 | // watchCount mustEqual 2 98 | // zkClient.delete("/tree/a") 99 | // Thread.sleep(50L) 100 | // children.size mustEqual 2 101 | // children must containAll(List("b", "c")) 102 | // watchCount mustEqual 3 103 | // zkClient.deleteRecursive("/tree") 104 | // } 105 | // 106 | // "watch a tree of nodes with data" in { 107 | // def mkNode(node : String) { 108 | // zkClient.create("/root/" + node, node.getBytes, CreateMode.EPHEMERAL) 109 | // } 110 | // var children : mutable.Map[String,String] = mutable.Map() 111 | // var watchCount = 0 112 | // def notifier(child : String) { 113 | // watchCount += 1 114 | // if (children.contains(child)) { 115 | // children(child) mustEqual child 116 | // } 117 | // } 118 | // zkClient.createPath("/root") 119 | // mkNode("a") 120 | // mkNode("b") 121 | // zkClient.watchChildrenWithData("/root", children, 122 | // {(b : Array[Byte]) => new String(b)}, notifier) 123 | // children.size mustEqual 2 124 | // children.keySet must containAll(List("a", "b")) 125 | // watchCount mustEqual 2 126 | // mkNode("c") 127 | // Thread.sleep(50L) 128 | // children.size mustEqual 3 129 | // children.keySet must containAll(List("a", "b", "c")) 130 | // watchCount mustEqual 3 131 | // zkClient.delete("/root/a") 132 | // Thread.sleep(50L) 133 | // children.size mustEqual 2 134 | // children.keySet must containAll(List("b", "c")) 135 | // watchCount mustEqual 4 136 | // zkClient.deleteRecursive("/root") 137 | // } 138 | // 139 | // } 140 | } 141 | --------------------------------------------------------------------------------