Browse Source

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 <dappavoo@twitter.com>

Reviewers: fangmin@apache.org, andor@apache.org

Closes #735 from dineshappavoo/ZOOKEEPER-3209
Dinesh Appavoo 6 years ago
parent
commit
2eb8dd0baf

+ 2 - 2
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"

+ 13 - 1
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>
 

+ 8 - 0
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 {

+ 14 - 0
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);
+    }
 }

+ 15 - 1
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);
                   }

+ 2 - 0
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;

+ 65 - 0
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;
@@ -2665,6 +2668,68 @@ public class ZooKeeper implements AutoCloseable {
         getChildren(path, watch ? watchManager.defaultWatcher : null, cb, ctx);
     }
 
+    /**
+     * 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

+ 2 - 0
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()) {

+ 73 - 0
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;
+    }
+}

+ 24 - 0
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

+ 1 - 0
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;

+ 4 - 0
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:

+ 2 - 0
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:

+ 235 - 0
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());
+        }
+    }
+}