You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zookeeper.apache.org by an...@apache.org on 2019/01/16 13:21:32 UTC

[zookeeper] branch master updated: ZOOKEEPER-3209: New `getEphemerals` api to get all the ephemeral nodes created by the session

This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/master by this push:
     new 2eb8dd0  ZOOKEEPER-3209: New `getEphemerals` api to get all the ephemeral nodes created by the session
2eb8dd0 is described below

commit 2eb8dd0baf91a1a7c09d76f22ff658009898ca0b
Author: Dinesh Appavoo <da...@twitter.com>
AuthorDate: Wed Jan 16 06:21:28 2019 -0700

    ZOOKEEPER-3209: New `getEphemerals` api to get all the ephemeral nodes created by the session
    
    See https://issues.apache.org/jira/browse/ZOOKEEPER-3209 for details about the API.
    
    New API `getEphemerals()` to get all the ephemeral nodes created by the session by providing the prefix path.
    
    * Get the prefix path as a input parameter and return a list of string (ephemeral nodes)
    * If the prefix path is `/` or empty return all the ephemeral nodes created by the session
    * Provide synchronous and asynchronous API's with same functionality
    
    Author: Dinesh Appavoo <da...@twitter.com>
    
    Reviewers: fangmin@apache.org, andor@apache.org
    
    Closes #735 from dineshappavoo/ZOOKEEPER-3209
---
 ivy.xml                                            |   4 +-
 .../resources/markdown/zookeeperProgrammers.md     |  14 +-
 zookeeper-jute/src/main/resources/zookeeper.jute   |   8 +
 .../java/org/apache/zookeeper/AsyncCallback.java   |  14 ++
 .../main/java/org/apache/zookeeper/ClientCnxn.java |  16 +-
 .../main/java/org/apache/zookeeper/ZooDefs.java    |   2 +
 .../main/java/org/apache/zookeeper/ZooKeeper.java  |  65 ++++++
 .../java/org/apache/zookeeper/ZooKeeperMain.java   |   2 +
 .../apache/zookeeper/cli/GetEphemeralsCommand.java |  73 +++++++
 .../zookeeper/server/FinalRequestProcessor.java    |  24 +++
 .../zookeeper/server/PrepRequestProcessor.java     |   1 +
 .../java/org/apache/zookeeper/server/Request.java  |   4 +
 .../apache/zookeeper/server/TraceFormatter.java    |   2 +
 .../org/apache/zookeeper/GetEphemeralsTest.java    | 235 +++++++++++++++++++++
 14 files changed, 460 insertions(+), 4 deletions(-)

diff --git a/ivy.xml b/ivy.xml
index a953317..b69df51 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -91,8 +91,8 @@
                 rev="${apache-rat-tasks.version}" conf="releaseaudit->default">
         <exclude org="commons-collections" module="commons-collections"/>
     </dependency>
-    <dependency org="commons-lang" name="commons-lang" 
-                rev="${commons-lang.version}" conf="releaseaudit->default"/>
+    <dependency org="commons-lang" name="commons-lang"
+                rev="${commons-lang.version}"/>
     <dependency org="commons-collections" name="commons-collections" 
                 rev="${commons-collections.version}" conf="releaseaudit->default"/>
     <dependency org="org.owasp" name="dependency-check-ant"
diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md b/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
index 4fa4159..47b7d1a 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperProgrammers.md
@@ -194,7 +194,19 @@ store pointers to the storage locations in ZooKeeper.
 ZooKeeper also has the notion of ephemeral nodes. These znodes
 exists as long as the session that created the znode is active. When
 the session ends the znode is deleted. Because of this behavior
-ephemeral znodes are not allowed to have children.
+ephemeral znodes are not allowed to have children. The list of ephemerals
+for the session can be retrieved using **getEphemerals()** api.
+
+##### getEphemerals()
+Retrieves the list of ephemeral nodes created by the session for the
+given path. If the path is empty, it will list all the ephemeral nodes
+for the session.
+**Use Case** - A sample use case might be, if the list of ephemeral
+nodes for the session need to be collected for duplicate data entry check
+and the nodes are created in sequential manner so you do not know the name
+for duplicate check. In that case, getEphemerals() api could be used to
+get the list of nodes for the session. This might be a typical use case
+for service discovery.
 
 <a name="Sequence+Nodes+--+Unique+Naming"></a>
 
diff --git a/zookeeper-jute/src/main/resources/zookeeper.jute b/zookeeper-jute/src/main/resources/zookeeper.jute
index 2533ddf..303d7ce 100644
--- a/zookeeper-jute/src/main/resources/zookeeper.jute
+++ b/zookeeper-jute/src/main/resources/zookeeper.jute
@@ -222,6 +222,14 @@ module org.apache.zookeeper.proto {
         ustring path;
         int type;
     }
+
+    class GetEphemeralsRequest {
+        ustring prefixPath;
+    }
+
+    class GetEphemeralsResponse {
+        vector<ustring> ephemerals;
+    }
 }
 
 module org.apache.zookeeper.server.quorum {
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/AsyncCallback.java b/zookeeper-server/src/main/java/org/apache/zookeeper/AsyncCallback.java
index c5529d7..cca7be4 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/AsyncCallback.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/AsyncCallback.java
@@ -328,4 +328,18 @@ public interface AsyncCallback {
         public void processResult(int rc, String path, Object ctx,
                 List<OpResult> opResults);
     }
+
+    /**
+     * This callback is used to process the getEphemerals results from
+     * a single getEphemerals call.
+     */
+    interface EphemeralsCallback extends AsyncCallback {
+        /**
+         *
+         * @param rc      The return code or the result of the call.
+         * @param ctx     Whatever context object that we passed to asynchronous calls.
+         * @param paths   The path that we passed to asynchronous calls.
+         */
+        public void processResult(int rc, Object ctx, List<String> paths);
+    }
 }
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java
index db2b486..5c4171a 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ClientCnxn.java
@@ -53,6 +53,7 @@ import org.apache.zookeeper.AsyncCallback.Children2Callback;
 import org.apache.zookeeper.AsyncCallback.ChildrenCallback;
 import org.apache.zookeeper.AsyncCallback.Create2Callback;
 import org.apache.zookeeper.AsyncCallback.DataCallback;
+import org.apache.zookeeper.AsyncCallback.EphemeralsCallback;
 import org.apache.zookeeper.AsyncCallback.MultiCallback;
 import org.apache.zookeeper.AsyncCallback.StatCallback;
 import org.apache.zookeeper.AsyncCallback.StringCallback;
@@ -78,6 +79,7 @@ import org.apache.zookeeper.proto.GetACLResponse;
 import org.apache.zookeeper.proto.GetChildren2Response;
 import org.apache.zookeeper.proto.GetChildrenResponse;
 import org.apache.zookeeper.proto.GetDataResponse;
+import org.apache.zookeeper.proto.GetEphemeralsResponse;
 import org.apache.zookeeper.proto.GetSASLRequest;
 import org.apache.zookeeper.proto.ReplyHeader;
 import org.apache.zookeeper.proto.RequestHeader;
@@ -557,6 +559,9 @@ public class ClientCnxn {
                     } else if (lcb.cb instanceof StringCallback) {
                         ((StringCallback) lcb.cb).processResult(lcb.rc,
                                 lcb.path, lcb.ctx, null);
+                    } else if (lcb.cb instanceof AsyncCallback.EphemeralsCallback) {
+                        ((AsyncCallback.EphemeralsCallback) lcb.cb).processResult(lcb.rc,
+                              lcb.ctx, null);
                     } else {
                         ((VoidCallback) lcb.cb).processResult(lcb.rc, lcb.path,
                                 lcb.ctx);
@@ -670,7 +675,16 @@ public class ClientCnxn {
                 	  } else {
                 		  cb.processResult(rc, clientPath, p.ctx, null);
                 	  }
-                  }  else if (p.cb instanceof VoidCallback) {
+                  } else if (p.response instanceof GetEphemeralsResponse) {
+                    EphemeralsCallback cb = (EphemeralsCallback) p.cb;
+                    GetEphemeralsResponse rsp = (GetEphemeralsResponse) p.response;
+                    if (rc == 0) {
+                      cb.processResult(rc, p.ctx, rsp.getEphemerals());
+                    } else {
+                      cb.processResult(rc, p.ctx, null);
+                    }
+                  }
+                  else if (p.cb instanceof VoidCallback) {
                       VoidCallback cb = (VoidCallback) p.cb;
                       cb.processResult(rc, clientPath, p.ctx);
                   }
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooDefs.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooDefs.java
index 97aa28a..a3b959e 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooDefs.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooDefs.java
@@ -81,6 +81,8 @@ public class ZooDefs {
 
         public final int sasl = 102;
 
+        public final int getEphemerals = 103;
+
         public final int createSession = -10;
 
         public final int closeSession = -11;
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
index 6cac98e..1475220 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeper.java
@@ -57,6 +57,9 @@ import org.apache.zookeeper.proto.GetChildrenRequest;
 import org.apache.zookeeper.proto.GetChildrenResponse;
 import org.apache.zookeeper.proto.GetDataRequest;
 import org.apache.zookeeper.proto.GetDataResponse;
+import org.apache.zookeeper.proto.GetEphemeralsRequest;
+import org.apache.zookeeper.proto.GetEphemeralsResponse;
+import org.apache.zookeeper.proto.ReconfigRequest;
 import org.apache.zookeeper.proto.RemoveWatchesRequest;
 import org.apache.zookeeper.proto.ReplyHeader;
 import org.apache.zookeeper.proto.RequestHeader;
@@ -2666,6 +2669,68 @@ public class ZooKeeper implements AutoCloseable {
     }
 
     /**
+     * Synchronously gets all the ephemeral nodes  created by this session.
+     *
+     * @since 3.6.0
+     *
+     */
+    public List<String> getEphemerals()
+        throws KeeperException, InterruptedException {
+        return getEphemerals("/");
+    }
+
+    /**
+     * Synchronously gets all the ephemeral nodes matching prefixPath
+     * created by this session.  If prefixPath is "/" then it returns all
+     * ephemerals
+     *
+     * @since 3.6.0
+     *
+     */
+    public List<String> getEphemerals(String prefixPath)
+        throws KeeperException, InterruptedException {
+        PathUtils.validatePath(prefixPath);
+        RequestHeader h = new RequestHeader();
+        h.setType(ZooDefs.OpCode.getEphemerals);
+        GetEphemeralsRequest request = new GetEphemeralsRequest(prefixPath);
+        GetEphemeralsResponse response = new GetEphemeralsResponse();
+        ReplyHeader r = cnxn.submitRequest(h, request, response, null);
+        if (r.getErr() != 0) {
+            throw KeeperException.create(KeeperException.Code.get(r.getErr()));
+        }
+        return response.getEphemerals();
+    }
+
+    /**
+     * Asynchronously gets all the ephemeral nodes matching prefixPath
+     * created by this session.  If prefixPath is "/" then it returns all
+     * ephemerals
+     *
+     * @since 3.6.0
+     *
+     */
+    public void getEphemerals(String prefixPath, AsyncCallback.EphemeralsCallback cb, Object ctx) {
+        PathUtils.validatePath(prefixPath);
+        RequestHeader h = new RequestHeader();
+        h.setType(ZooDefs.OpCode.getEphemerals);
+        GetEphemeralsRequest request = new GetEphemeralsRequest(prefixPath);
+        GetEphemeralsResponse response = new GetEphemeralsResponse();
+        cnxn.queuePacket(h, new ReplyHeader(), request, response, cb,
+            null, null, ctx, null);
+    }
+
+    /**
+     * Asynchronously gets all the ephemeral nodes created by this session.
+     * ephemerals
+     *
+     * @since 3.6.0
+     *
+     */
+    public void getEphemerals(AsyncCallback.EphemeralsCallback cb, Object ctx) {
+        getEphemerals("/", cb, ctx);
+    }
+
+    /**
      * Asynchronous sync. Flushes channel between process and leader.
      * @param path
      * @param cb a handler for the callback
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java
index 300143a..7dbdf2f 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java
@@ -37,6 +37,7 @@ import java.util.NoSuchElementException;
 import org.apache.yetus.audience.InterfaceAudience;
 import org.apache.zookeeper.cli.CliException;
 import org.apache.zookeeper.cli.CommandNotFoundException;
+import org.apache.zookeeper.cli.GetEphemeralsCommand;
 import org.apache.zookeeper.cli.MalformedCommandException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -122,6 +123,7 @@ public class ZooKeeperMain {
         new ReconfigCommand().addToMap(commandMapCli);
         new GetConfigCommand().addToMap(commandMapCli);
         new RemoveWatchesCommand().addToMap(commandMapCli);
+        new GetEphemeralsCommand().addToMap(commandMapCli);
         
         // add all to commandMap
         for (Entry<String, CliCommand> entry : commandMapCli.entrySet()) {
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/cli/GetEphemeralsCommand.java b/zookeeper-server/src/main/java/org/apache/zookeeper/cli/GetEphemeralsCommand.java
new file mode 100644
index 0000000..e045112
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/cli/GetEphemeralsCommand.java
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.zookeeper.cli;
+
+import java.util.List;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.Parser;
+import org.apache.commons.cli.PosixParser;
+import org.apache.zookeeper.KeeperException;
+
+/**
+ * getEphemerals command for CLI
+ */
+public class GetEphemeralsCommand extends CliCommand {
+    private static Options options = new Options();
+    private String[] args;
+
+    public GetEphemeralsCommand() {
+        super("getEphemerals", "path");
+    }
+
+    @Override
+    public CliCommand parse(String[] cmdArgs) throws CliParseException {
+        Parser parser = new PosixParser();
+        CommandLine cl;
+        try {
+            cl = parser.parse(options, cmdArgs);
+        } catch (ParseException ex) {
+            throw new CliParseException(ex);
+        }
+        args = cl.getArgs();
+
+        return this;
+    }
+
+    @Override
+    public boolean exec() throws CliException {
+        String path;
+        List<String> ephemerals;
+        try {
+            if (args.length < 2) {
+                // gets all the ephemeral nodes for the session
+                ephemerals = zk.getEphemerals();
+            } else {
+                path = args[1];
+                ephemerals = zk.getEphemerals(path);
+            }
+        } catch (IllegalArgumentException ex) {
+            throw new MalformedPathException(ex.getMessage());
+        } catch (KeeperException|InterruptedException ex) {
+            throw new CliWrapperException(ex);
+        }
+        out.println(ephemerals);
+        return false;
+    }
+}
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/FinalRequestProcessor.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/FinalRequestProcessor.java
index d022193..2c8c5b2 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/FinalRequestProcessor.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/FinalRequestProcessor.java
@@ -18,6 +18,7 @@
 
 package org.apache.zookeeper.server;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.jute.Record;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.KeeperException.Code;
@@ -48,6 +49,8 @@ import org.apache.zookeeper.proto.GetChildrenRequest;
 import org.apache.zookeeper.proto.GetChildrenResponse;
 import org.apache.zookeeper.proto.GetDataRequest;
 import org.apache.zookeeper.proto.GetDataResponse;
+import org.apache.zookeeper.proto.GetEphemeralsRequest;
+import org.apache.zookeeper.proto.GetEphemeralsResponse;
 import org.apache.zookeeper.proto.RemoveWatchesRequest;
 import org.apache.zookeeper.proto.ReplyHeader;
 import org.apache.zookeeper.proto.SetACLResponse;
@@ -65,8 +68,10 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 /**
  * This Request processor actually applies any transaction associated with a
@@ -441,6 +446,25 @@ public class FinalRequestProcessor implements RequestProcessor {
                 }
                 break;
             }
+            case OpCode.getEphemerals: {
+                lastOp = "GETE";
+                GetEphemeralsRequest getEphemerals = new GetEphemeralsRequest();
+                ByteBufferInputStream.byteBuffer2Record(request.request, getEphemerals);
+                String prefixPath = getEphemerals.getPrefixPath();
+                Set<String> allEphems = zks.getZKDatabase().getDataTree().getEphemerals(request.sessionId);
+                List<String> ephemerals = new ArrayList<>();
+                if (StringUtils.isBlank(prefixPath) || "/".equals(prefixPath.trim())) {
+                    ephemerals.addAll(allEphems);
+                } else {
+                    for (String path: allEphems) {
+                        if(path.startsWith(prefixPath)) {
+                            ephemerals.add(path);
+                        }
+                    }
+                }
+                rsp = new GetEphemeralsResponse(ephemerals);
+                break;
+            }
             }
         } catch (SessionMovedException e) {
             // session moved is a connection level error, we need to tear
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/PrepRequestProcessor.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/PrepRequestProcessor.java
index 57eaf1e..be87530 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/PrepRequestProcessor.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/PrepRequestProcessor.java
@@ -859,6 +859,7 @@ public class PrepRequestProcessor extends ZooKeeperCriticalThread implements
             case OpCode.setWatches:
             case OpCode.checkWatches:
             case OpCode.removeWatches:
+            case OpCode.getEphemerals:
                 zks.sessionTracker.checkSession(request.sessionId,
                         request.getOwner());
                 break;
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/Request.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/Request.java
index ede9280..20877c9 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/Request.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/Request.java
@@ -147,6 +147,7 @@ public class Request {
         case OpCode.getChildren:
         case OpCode.getChildren2:
         case OpCode.getData:
+        case OpCode.getEphemerals:
         case OpCode.multi:
         case OpCode.ping:
         case OpCode.reconfig:
@@ -169,6 +170,7 @@ public class Request {
         case OpCode.getChildren:
         case OpCode.getChildren2:
         case OpCode.getData:
+        case OpCode.getEphemerals:
             return false;
         case OpCode.create:
         case OpCode.create2:
@@ -229,6 +231,8 @@ public class Request {
             return "getChildren";
         case OpCode.getChildren2:
             return "getChildren2";
+        case OpCode.getEphemerals:
+            return "getEphemerals";
         case OpCode.ping:
             return "ping";
         case OpCode.createSession:
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java
index f4c1383..dff6dfd 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java
@@ -61,6 +61,8 @@ public class TraceFormatter {
             return "getChildren";
         case OpCode.getChildren2:
             return "getChildren2";
+        case OpCode.getEphemerals:
+            return "getEphemerals";
         case OpCode.ping:
             return "ping";
         case OpCode.createSession:
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/GetEphemeralsTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/GetEphemeralsTest.java
new file mode 100644
index 0000000..355223f
--- /dev/null
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/GetEphemeralsTest.java
@@ -0,0 +1,235 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zookeeper;
+
+import org.apache.zookeeper.AsyncCallback;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZooDefs.Ids;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.test.ClientBase;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class GetEphemeralsTest extends ClientBase {
+    private static final String BASE = "/base";
+    private static final int PERSISTENT_CNT = 2;
+    private static final int EPHEMERAL_CNT = 2;
+    private static final String NEWLINE = System.getProperty("line.separator");
+    private String[] expected;
+    private ZooKeeper zk;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        zk = createClient();
+        expected = generatePaths(PERSISTENT_CNT, EPHEMERAL_CNT);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+
+        zk.close();
+    }
+
+    @Test
+    public void testGetEphemeralsSync() throws KeeperException, InterruptedException {
+        List<String> actual = zk.getEphemerals();
+        Assert.assertEquals( "Expected ephemeral count for allPaths",
+               actual.size() , expected.length);
+        for (int i = 0; i < expected.length; i++) {
+            String path = expected[i];
+            Assert.assertTrue(
+                    String.format("Path=%s exists in get All Ephemerals list ", path),
+                    actual.contains(path));
+        }
+    }
+
+    @Test
+    public void testGetEphemeralsSyncByPath() throws KeeperException, InterruptedException {
+        final String prefixPath = BASE + 0;
+        List<String> actual = zk.getEphemerals(prefixPath);
+        Assert.assertEquals( "Expected ephemeral count for allPaths",
+                actual.size() , EPHEMERAL_CNT);
+        for (int i = 0; i < EPHEMERAL_CNT; i++) {
+            String path = expected[i];
+            Assert.assertTrue(String.format("Path=%s exists in getEphemerals(%s) list ",
+                path, prefixPath), actual.contains(path));
+        }
+    }
+
+    @Test
+    public void testGetEphemerals()
+        throws IOException, KeeperException, InterruptedException {
+
+        final CountDownLatch doneProcessing = new CountDownLatch(1);
+        final List<String> unexpectedBehavior = new ArrayList<String>();
+        zk.getEphemerals(new AsyncCallback.EphemeralsCallback() {
+            @Override
+            public void processResult(int rc, Object ctx, List<String> paths) {
+                if (paths == null ) {
+                    unexpectedBehavior.add(String.format("Expected ephemeral count for" +
+                            " allPaths to be %d but was null", expected.length));
+                } else if (paths.size() != expected.length) {
+                    unexpectedBehavior.add(
+                        String.format("Expected ephemeral count for allPaths to be %d but was %d",
+                            expected.length, paths.size()));
+                }
+                for (int i = 0; i < expected.length; i++) {
+                    String path = expected[i];
+                    if (!paths.contains(path)) {
+                        unexpectedBehavior.add(
+                            String.format("Path=%s exists in getEphemerals list ", path));
+                    }
+                }
+                doneProcessing.countDown();
+            }
+        }, null);
+        long waitForCallbackSecs = 2l;
+        if (!doneProcessing.await(waitForCallbackSecs, TimeUnit.SECONDS)) {
+            Assert.fail(String.format("getEphemerals didn't callback within %d seconds",
+                waitForCallbackSecs));
+        }
+        checkForUnexpectedBehavior(unexpectedBehavior);
+
+    }
+
+    @Test
+    public void testGetEphemeralsByPath()
+            throws IOException, KeeperException, InterruptedException {
+
+        final CountDownLatch doneProcessing = new CountDownLatch(1);
+        final String checkPath = BASE + "0";
+        final List<String> unexpectedBehavior = new ArrayList<String>();
+        zk.getEphemerals(checkPath, new AsyncCallback.EphemeralsCallback() {
+            @Override
+            public void processResult(int rc, Object ctx, List<String> paths) {
+                if (paths == null ) {
+                    unexpectedBehavior.add(
+                            String.format("Expected ephemeral count for %s to be %d but was null",
+                                    checkPath, expected.length));
+                } else if (paths.size() != EPHEMERAL_CNT) {
+                    unexpectedBehavior.add(
+                            String.format("Expected ephemeral count for %s to be %d but was %d",
+                                    checkPath, EPHEMERAL_CNT, paths.size()));
+                }
+                for (int i = 0; i < EPHEMERAL_CNT; i++) {
+                    String path = expected[i];
+                    if(! paths.contains(path)) {
+                        unexpectedBehavior.add(String.format("Expected path=%s didn't exist " +
+                                        "in getEphemerals list.", path));
+                    }
+                }
+                doneProcessing.countDown();
+            }
+        }, null);
+        long waitForCallbackSecs = 2l;
+        if (!doneProcessing.await(waitForCallbackSecs, TimeUnit.SECONDS)) {
+            Assert.fail(String.format("getEphemerals(%s) didn't callback within %d seconds",
+                    checkPath, waitForCallbackSecs));
+        }
+        checkForUnexpectedBehavior(unexpectedBehavior);
+    }
+
+    @Test
+    public void testGetEphemeralsEmpty()
+            throws IOException, KeeperException, InterruptedException {
+
+        final CountDownLatch doneProcessing = new CountDownLatch(1);
+        final String checkPath = "/unknownPath";
+        final int expectedSize = 0;
+        final List<String> unexpectedBehavior = new ArrayList<String>();
+        zk.getEphemerals(checkPath, new AsyncCallback.EphemeralsCallback() {
+            @Override
+            public void processResult(int rc, Object ctx, List<String> paths) {
+                if (paths == null ) {
+                    unexpectedBehavior.add(
+                            String.format("Expected ephemeral count for %s to be %d but was null",
+                                    checkPath, expectedSize));
+                } else if (paths.size() != expectedSize) {
+                    unexpectedBehavior.add(
+                        String.format("Expected ephemeral count for %s to be %d but was %d",
+                                checkPath, expectedSize, paths.size()));
+                }
+                doneProcessing.countDown();
+            }
+        }, null);
+        long waitForCallbackSecs = 2l;
+        if (!doneProcessing.await(waitForCallbackSecs, TimeUnit.SECONDS)) {
+            Assert.fail(String.format("getEphemerals(%s) didn't callback within %d seconds",
+                    checkPath, waitForCallbackSecs));
+        }
+        checkForUnexpectedBehavior(unexpectedBehavior);
+    }
+
+    @Test
+    public void testGetEphemeralsErrors() throws KeeperException {
+        try {
+            zk.getEphemerals(null, null, null);
+            Assert.fail("Should have thrown a IllegalArgumentException for a null prefixPath");
+        } catch (IllegalArgumentException e) {
+            //pass
+        }
+
+        try {
+            zk.getEphemerals("no leading slash", null, null);
+            Assert.fail("Should have thrown a IllegalArgumentException " +
+                    "for a prefix with no leading slash");
+        } catch (IllegalArgumentException e) {
+            //pass
+        }
+    }
+
+    private String[] generatePaths(int persistantCnt, int ephemeralCnt)
+            throws KeeperException, InterruptedException {
+
+        final String[] expected = new String[persistantCnt * ephemeralCnt];
+        for (int p = 0; p < persistantCnt; p++) {
+            String base = BASE + p;
+            zk.create(base, base.getBytes(), Ids.OPEN_ACL_UNSAFE,
+                CreateMode.PERSISTENT);
+            for (int e = 0; e < ephemeralCnt; e++) {
+                String ephem = base + "/ephem" + e;
+                zk.create(ephem, ephem.getBytes(), Ids.OPEN_ACL_UNSAFE,
+                    CreateMode.EPHEMERAL);
+                expected[p * ephemeralCnt + e] = ephem;
+            }
+        }
+        return expected;
+    }
+
+    private void checkForUnexpectedBehavior(List<String> unexpectedBehavior) {
+        if (unexpectedBehavior.size() > 0) {
+            StringBuilder b = new StringBuilder("The test failed for the following reasons:");
+            b.append(NEWLINE);
+            for (String error : unexpectedBehavior) {
+                b.append("ERROR: ").append(error).append(NEWLINE);
+            }
+            Assert.fail(b.toString());
+        }
+    }
+}