org.apache.bookkeeper.tools.BookKeeperTools.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.bookkeeper.tools.BookKeeperTools.java

Source

package org.apache.bookkeeper.tools;

/*
 * 
 * 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.
 * 
 */

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.bookkeeper.client.BKException;
import org.apache.bookkeeper.client.BookKeeper;
import org.apache.bookkeeper.client.LedgerEntry;
import org.apache.bookkeeper.client.LedgerHandle;
import org.apache.bookkeeper.client.AsyncCallback.OpenCallback;
import org.apache.bookkeeper.client.AsyncCallback.ReadCallback;
import org.apache.bookkeeper.client.AsyncCallback.RecoverCallback;
import org.apache.bookkeeper.client.BookKeeper.DigestType;
import org.apache.bookkeeper.proto.BookkeeperInternalCallbacks.WriteCallback;
import org.apache.log4j.Logger;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.data.Stat;
import org.jboss.netty.buffer.ChannelBuffer;

/**
 * Provides Admin Tools to manage the BookKeeper cluster.
 * 
 */
public class BookKeeperTools {

    private static Logger LOG = Logger.getLogger(BookKeeperTools.class);

    // ZK client instance
    private ZooKeeper zk;
    // ZK ledgers related String constants
    static final String LEDGERS_PATH = "/ledgers";
    static final String LEDGER_NODE_PREFIX = "L";
    static final String AVAILABLE_NODE = "available";
    static final String BOOKIES_PATH = LEDGERS_PATH + "/" + AVAILABLE_NODE;
    static final String COLON = ":";

    // BookKeeper client instance
    private BookKeeper bkc;

    /*
     * Random number generator used to choose an available bookie server to
     * replicate data from a dead bookie.
     */
    private Random rand = new Random();

    /*
     * For now, assume that all ledgers were created with the same DigestType
     * and password. In the future, this admin tool will need to know for each
     * ledger, what was the DigestType and password used to create it before it
     * can open it. These values will come from System properties, though hard
     * coded defaults are defined here.
     */
    private DigestType DIGEST_TYPE = DigestType
            .valueOf(System.getProperty("digestType", DigestType.CRC32.toString()));
    private byte[] PASSWD = System.getProperty("passwd", "").getBytes();

    /**
     * Constructor that takes in a ZooKeeper servers connect string so we know
     * how to connect to ZooKeeper to retrieve information about the BookKeeper
     * cluster. We need this before we can do any type of admin operations on
     * the BookKeeper cluster.
     * 
     * @param zkServers
     *            Comma separated list of hostname:port pairs for the ZooKeeper
     *            servers cluster.
     * @throws IOException
     *             Throws this exception if there is an error instantiating the
     *             ZooKeeper client.
     * @throws InterruptedException
     *             Throws this exception if there is an error instantiating the
     *             BookKeeper client.
     * @throws KeeperException
     *             Throws this exception if there is an error instantiating the
     *             BookKeeper client.
     */
    public BookKeeperTools(String zkServers) throws IOException, InterruptedException, KeeperException {
        // Create the ZooKeeper client instance
        zk = new ZooKeeper(zkServers, 10000, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Process: " + event.getType() + " " + event.getPath());
                }
            }
        });
        // Create the BookKeeper client instance
        bkc = new BookKeeper(zk);
    }

    /**
     * Shutdown method to gracefully release resources that this class uses.
     * 
     * @throws InterruptedException
     *             if there is an error shutting down the clients that this
     *             class uses.
     */
    public void shutdown() throws InterruptedException {
        bkc.halt();
        zk.close();
    }

    /**
     * This is a multi callback object for bookie recovery that waits for all of
     * the multiple async operations to complete. If any fail, then we invoke
     * the final callback with a BK LedgerRecoveryException.
     */
    class MultiCallback implements AsyncCallback.VoidCallback {
        // Number of expected callbacks
        final int expected;
        // Final callback and the corresponding context to invoke
        final AsyncCallback.VoidCallback cb;
        final Object context;
        // This keeps track of how many operations have completed
        final AtomicInteger done = new AtomicInteger();
        // List of the exceptions from operations that completed unsuccessfully
        final LinkedBlockingQueue<Integer> exceptions = new LinkedBlockingQueue<Integer>();

        MultiCallback(int expected, AsyncCallback.VoidCallback cb, Object context) {
            this.expected = expected;
            this.cb = cb;
            this.context = context;
            if (expected == 0) {
                cb.processResult(Code.OK.intValue(), null, context);
            }
        }

        private void tick() {
            if (done.incrementAndGet() == expected) {
                if (exceptions.isEmpty()) {
                    cb.processResult(Code.OK.intValue(), null, context);
                } else {
                    cb.processResult(BKException.Code.LedgerRecoveryException, null, context);
                }
            }
        }

        @Override
        public void processResult(int rc, String path, Object ctx) {
            if (rc != Code.OK.intValue()) {
                LOG.error("BK error recovering ledger data", BKException.create(rc));
                exceptions.add(rc);
            }
            tick();
        }

    }

    /**
     * Method to get the input ledger's digest type. For now, this is just a
     * placeholder function since there is no way we can get this information
     * easily. In the future, BookKeeper should store this ledger metadata
     * somewhere such that an admin tool can access it.
     * 
     * @param ledgerId
     *            LedgerId we are retrieving the digestType for.
     * @return DigestType for the input ledger
     */
    private DigestType getLedgerDigestType(long ledgerId) {
        return DIGEST_TYPE;
    }

    /**
     * Method to get the input ledger's password. For now, this is just a
     * placeholder function since there is no way we can get this information
     * easily. In the future, BookKeeper should store this ledger metadata
     * somewhere such that an admin tool can access it.
     * 
     * @param ledgerId
     *            LedgerId we are retrieving the password for.
     * @return Password for the input ledger
     */
    private byte[] getLedgerPasswd(long ledgerId) {
        return PASSWD;
    }

    // Object used for calling async methods and waiting for them to complete.
    class SyncObject {
        boolean value;

        public SyncObject() {
            value = false;
        }
    }

    /**
     * Synchronous method to rebuild and recover the ledger fragments data that
     * was stored on the source bookie. That bookie could have failed completely
     * and now the ledger data that was stored on it is under replicated. An
     * optional destination bookie server could be given if we want to copy all
     * of the ledger fragments data on the failed source bookie to it.
     * Otherwise, we will just randomly distribute the ledger fragments to the
     * active set of bookies, perhaps based on load. All ZooKeeper ledger
     * metadata will be updated to point to the new bookie(s) that contain the
     * replicated ledger fragments.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param bookieDest
     *            Optional destination bookie that if passed, we will copy all
     *            of the ledger fragments from the source bookie over to it.
     */
    public void recoverBookieData(final InetSocketAddress bookieSrc, final InetSocketAddress bookieDest)
            throws InterruptedException {
        SyncObject sync = new SyncObject();
        // Call the async method to recover bookie data.
        asyncRecoverBookieData(bookieSrc, bookieDest, new RecoverCallback() {
            @Override
            public void recoverComplete(int rc, Object ctx) {
                LOG.info("Recover bookie operation completed with rc: " + rc);
                SyncObject syncObj = (SyncObject) ctx;
                synchronized (syncObj) {
                    syncObj.value = true;
                    syncObj.notify();
                }
            }
        }, sync);

        // Wait for the async method to complete.
        synchronized (sync) {
            while (sync.value == false) {
                sync.wait();
            }
        }
    }

    /**
     * Async method to rebuild and recover the ledger fragments data that was
     * stored on the source bookie. That bookie could have failed completely and
     * now the ledger data that was stored on it is under replicated. An
     * optional destination bookie server could be given if we want to copy all
     * of the ledger fragments data on the failed source bookie to it.
     * Otherwise, we will just randomly distribute the ledger fragments to the
     * active set of bookies, perhaps based on load. All ZooKeeper ledger
     * metadata will be updated to point to the new bookie(s) that contain the
     * replicated ledger fragments.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param bookieDest
     *            Optional destination bookie that if passed, we will copy all
     *            of the ledger fragments from the source bookie over to it.
     * @param cb
     *            RecoverCallback to invoke once all of the data on the dead
     *            bookie has been recovered and replicated.
     * @param context
     *            Context for the RecoverCallback to call.
     */
    public void asyncRecoverBookieData(final InetSocketAddress bookieSrc, final InetSocketAddress bookieDest,
            final RecoverCallback cb, final Object context) {
        // Sync ZK to make sure we're reading the latest bookie/ledger data.
        zk.sync(LEDGERS_PATH, new AsyncCallback.VoidCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx) {
                if (rc != Code.OK.intValue()) {
                    LOG.error("ZK error syncing: ", KeeperException.create(KeeperException.Code.get(rc), path));
                    cb.recoverComplete(BKException.Code.ZKException, context);
                    return;
                }
                getAvailableBookies(bookieSrc, bookieDest, cb, context);
            };
        }, null);
    }

    /**
     * This method asynchronously gets the set of available Bookies that the
     * dead input bookie's data will be copied over into. If the user passed in
     * a specific destination bookie, then just use that one. Otherwise, we'll
     * randomly pick one of the other available bookies to use for each ledger
     * fragment we are replicating.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param bookieDest
     *            Optional destination bookie that if passed, we will copy all
     *            of the ledger fragments from the source bookie over to it.
     * @param cb
     *            RecoverCallback to invoke once all of the data on the dead
     *            bookie has been recovered and replicated.
     * @param context
     *            Context for the RecoverCallback to call.
     */
    private void getAvailableBookies(final InetSocketAddress bookieSrc, final InetSocketAddress bookieDest,
            final RecoverCallback cb, final Object context) {
        final List<InetSocketAddress> availableBookies = new LinkedList<InetSocketAddress>();
        if (bookieDest != null) {
            availableBookies.add(bookieDest);
            // Now poll ZK to get the active ledgers
            getActiveLedgers(bookieSrc, bookieDest, cb, context, availableBookies);
        } else {
            zk.getChildren(BOOKIES_PATH, null, new AsyncCallback.ChildrenCallback() {
                @Override
                public void processResult(int rc, String path, Object ctx, List<String> children) {
                    if (rc != Code.OK.intValue()) {
                        LOG.error("ZK error getting bookie nodes: ",
                                KeeperException.create(KeeperException.Code.get(rc), path));
                        cb.recoverComplete(BKException.Code.ZKException, context);
                        return;
                    }
                    for (String bookieNode : children) {
                        String parts[] = bookieNode.split(COLON);
                        if (parts.length < 2) {
                            LOG.error("Bookie Node retrieved from ZK has invalid name format: " + bookieNode);
                            cb.recoverComplete(BKException.Code.ZKException, context);
                            return;
                        }
                        availableBookies.add(new InetSocketAddress(parts[0], Integer.parseInt(parts[1])));
                    }
                    // Now poll ZK to get the active ledgers
                    getActiveLedgers(bookieSrc, bookieDest, cb, context, availableBookies);
                }
            }, null);
        }
    }

    /**
     * This method asynchronously polls ZK to get the current set of active
     * ledgers. From this, we can open each ledger and look at the metadata to
     * determine if any of the ledger fragments for it were stored at the dead
     * input bookie.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param bookieDest
     *            Optional destination bookie that if passed, we will copy all
     *            of the ledger fragments from the source bookie over to it.
     * @param cb
     *            RecoverCallback to invoke once all of the data on the dead
     *            bookie has been recovered and replicated.
     * @param context
     *            Context for the RecoverCallback to call.
     * @param availableBookies
     *            List of Bookie Servers that are available to use for
     *            replicating data on the failed bookie. This could contain a
     *            single bookie server if the user explicitly chose a bookie
     *            server to replicate data to.
     */
    private void getActiveLedgers(final InetSocketAddress bookieSrc, final InetSocketAddress bookieDest,
            final RecoverCallback cb, final Object context, final List<InetSocketAddress> availableBookies) {
        zk.getChildren(LEDGERS_PATH, null, new AsyncCallback.ChildrenCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, List<String> children) {
                if (rc != Code.OK.intValue()) {
                    LOG.error("ZK error getting ledger nodes: ",
                            KeeperException.create(KeeperException.Code.get(rc), path));
                    cb.recoverComplete(BKException.Code.ZKException, context);
                    return;
                }
                // Wrapper class around the RecoverCallback so it can be used
                // as the final VoidCallback to invoke within the MultiCallback.
                class RecoverCallbackWrapper implements AsyncCallback.VoidCallback {
                    final RecoverCallback cb;

                    RecoverCallbackWrapper(RecoverCallback cb) {
                        this.cb = cb;
                    }

                    @Override
                    public void processResult(int rc, String path, Object ctx) {
                        cb.recoverComplete(rc, ctx);
                    }
                }
                // Recover each of the ledgers asynchronously
                MultiCallback ledgerMcb = new MultiCallback(children.size(), new RecoverCallbackWrapper(cb),
                        context);
                for (final String ledgerNode : children) {
                    recoverLedger(bookieSrc, ledgerNode, ledgerMcb, availableBookies);
                }
            }
        }, null);
    }

    /**
     * This method asynchronously recovers a given ledger if any of the ledger
     * entries were stored on the failed bookie.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param ledgerNode
     *            Ledger Node name as retrieved from ZooKeeper we want to
     *            recover.
     * @param ledgerMcb
     *            MultiCallback to invoke once we've recovered the current
     *            ledger.
     * @param availableBookies
     *            List of Bookie Servers that are available to use for
     *            replicating data on the failed bookie. This could contain a
     *            single bookie server if the user explicitly chose a bookie
     *            server to replicate data to.
     */
    private void recoverLedger(final InetSocketAddress bookieSrc, final String ledgerNode,
            final MultiCallback ledgerMcb, final List<InetSocketAddress> availableBookies) {
        /*
         * The available node is also stored in this path so ignore that. That
         * node is the path for the set of available Bookie Servers.
         */
        if (ledgerNode.equals(AVAILABLE_NODE)) {
            ledgerMcb.processResult(BKException.Code.OK, null, null);
            return;
        }
        // Parse out the ledgerId from the ZK ledger node.
        String parts[] = ledgerNode.split(LEDGER_NODE_PREFIX);
        if (parts.length < 2) {
            LOG.error("Ledger Node retrieved from ZK has invalid name format: " + ledgerNode);
            ledgerMcb.processResult(BKException.Code.ZKException, null, null);
            return;
        }
        final long lId;
        try {
            lId = Long.parseLong(parts[parts.length - 1]);
        } catch (NumberFormatException e) {
            LOG.error("Error retrieving ledgerId from ledgerNode: " + ledgerNode, e);
            ledgerMcb.processResult(BKException.Code.ZKException, null, null);
            return;
        }
        /*
         * For the current ledger, open it to retrieve the LedgerHandle. This
         * will contain the LedgerMetadata indicating which bookie servers the
         * ledger fragments are stored on. Check if any of the ledger fragments
         * for the current ledger are stored on the input dead bookie.
         */
        DigestType digestType = getLedgerDigestType(lId);
        byte[] passwd = getLedgerPasswd(lId);
        bkc.asyncOpenLedger(lId, digestType, passwd, new OpenCallback() {
            @Override
            public void openComplete(int rc, final LedgerHandle lh, Object ctx) {
                if (rc != Code.OK.intValue()) {
                    LOG.error("BK error opening ledger: " + lId, BKException.create(rc));
                    ledgerMcb.processResult(rc, null, null);
                    return;
                }
                /*
                 * This List stores the ledger fragments to recover indexed by
                 * the start entry ID for the range. The ensembles TreeMap is
                 * keyed off this.
                 */
                final List<Long> ledgerFragmentsToRecover = new LinkedList<Long>();
                /*
                 * This Map will store the start and end entry ID values for
                 * each of the ledger fragment ranges. The only exception is the
                 * current active fragment since it has no end yet. In the event
                 * of a bookie failure, a new ensemble is created so the current
                 * ensemble should not contain the dead bookie we are trying to
                 * recover.
                 */
                Map<Long, Long> ledgerFragmentsRange = new HashMap<Long, Long>();
                Long curEntryId = null;
                for (Map.Entry<Long, ArrayList<InetSocketAddress>> entry : lh.getLedgerMetadata().getEnsembles()
                        .entrySet()) {
                    if (curEntryId != null)
                        ledgerFragmentsRange.put(curEntryId, entry.getKey() - 1);
                    curEntryId = entry.getKey();
                    if (entry.getValue().contains(bookieSrc)) {
                        /*
                         * Current ledger fragment has entries stored on the
                         * dead bookie so we'll need to recover them.
                         */
                        ledgerFragmentsToRecover.add(entry.getKey());
                    }
                }
                /*
                 * See if this current ledger contains any ledger fragment that
                 * needs to be re-replicated. If not, then just invoke the
                 * multiCallback and return.
                 */
                if (ledgerFragmentsToRecover.size() == 0) {
                    ledgerMcb.processResult(BKException.Code.OK, null, null);
                    return;
                }
                /*
                 * We have ledger fragments that need to be re-replicated to a
                 * new bookie. Choose one randomly from the available set of
                 * bookies.
                 */
                final InetSocketAddress newBookie = availableBookies.get(rand.nextInt(availableBookies.size()));

                /*
                 * Wrapper class around the ledger MultiCallback. Once all
                 * ledger fragments for the ledger have been replicated to a new
                 * bookie, we need to update ZK with this new metadata to point
                 * to the new bookie instead of the old dead one. That should be
                 * done at the end prior to invoking the ledger MultiCallback.
                 */
                class LedgerMultiCallbackWrapper implements AsyncCallback.VoidCallback {
                    final MultiCallback ledgerMcb;

                    LedgerMultiCallbackWrapper(MultiCallback ledgerMcb) {
                        this.ledgerMcb = ledgerMcb;
                    }

                    @Override
                    public void processResult(int rc, String path, Object ctx) {
                        if (rc != Code.OK.intValue()) {
                            LOG.error("BK error replicating ledger fragments for ledger: " + lId,
                                    BKException.create(rc));
                            ledgerMcb.processResult(rc, null, null);
                            return;
                        }
                        /*
                         * Update the ledger metadata's ensemble info to point
                         * to the new bookie.
                         */
                        for (final Long startEntryId : ledgerFragmentsToRecover) {
                            ArrayList<InetSocketAddress> ensemble = lh.getLedgerMetadata().getEnsembles()
                                    .get(startEntryId);
                            int deadBookieIndex = ensemble.indexOf(bookieSrc);
                            ensemble.remove(deadBookieIndex);
                            ensemble.add(deadBookieIndex, newBookie);
                        }
                        lh.writeLedgerConfig(new AsyncCallback.StatCallback() {
                            @Override
                            public void processResult(int rc, String path, Object ctx, Stat stat) {
                                if (rc != Code.OK.intValue()) {
                                    LOG.error(
                                            "ZK error updating ledger config metadata for ledgerId: " + lh.getId(),
                                            KeeperException.create(KeeperException.Code.get(rc), path));
                                } else {
                                    LOG.info("Updated ZK for ledgerId: (" + lh.getId()
                                            + ") to point ledger fragments from old dead bookie: (" + bookieSrc
                                            + ") to new bookie: (" + newBookie + ")");
                                }
                                /*
                                 * Pass the return code result up the chain with
                                 * the parent callback.
                                 */
                                ledgerMcb.processResult(rc, null, null);
                            }
                        }, null);
                    }
                }

                /*
                 * Now recover all of the necessary ledger fragments
                 * asynchronously using a MultiCallback for every fragment.
                 */
                MultiCallback ledgerFragmentMcb = new MultiCallback(ledgerFragmentsToRecover.size(),
                        new LedgerMultiCallbackWrapper(ledgerMcb), null);
                for (final Long startEntryId : ledgerFragmentsToRecover) {
                    Long endEntryId = ledgerFragmentsRange.get(startEntryId);
                    recoverLedgerFragment(bookieSrc, lh, startEntryId, endEntryId, ledgerFragmentMcb, newBookie);
                }
            }
        }, null);
    }

    /**
     * This method asynchronously recovers a ledger fragment which is a
     * contiguous portion of a ledger that was stored in an ensemble that
     * included the failed bookie.
     * 
     * @param bookieSrc
     *            Source bookie that had a failure. We want to replicate the
     *            ledger fragments that were stored there.
     * @param lh
     *            LedgerHandle for the ledger
     * @param startEntryId
     *            Start entry Id for the ledger fragment
     * @param endEntryId
     *            End entry Id for the ledger fragment
     * @param ledgerFragmentMcb
     *            MultiCallback to invoke once we've recovered the current
     *            ledger fragment.
     * @param newBookie
     *            New bookie we want to use to recover and replicate the ledger
     *            entries that were stored on the failed bookie.
     */
    private void recoverLedgerFragment(final InetSocketAddress bookieSrc, final LedgerHandle lh,
            final Long startEntryId, final Long endEntryId, final MultiCallback ledgerFragmentMcb,
            final InetSocketAddress newBookie) {
        if (endEntryId == null) {
            /*
             * Ideally this should never happen if bookie failure is taken care
             * of properly. Nothing we can do though in this case.
             */
            LOG.warn("Dead bookie (" + bookieSrc + ") is still part of the current active ensemble for ledgerId: "
                    + lh.getId());
            ledgerFragmentMcb.processResult(BKException.Code.OK, null, null);
            return;
        }

        ArrayList<InetSocketAddress> curEnsemble = lh.getLedgerMetadata().getEnsembles().get(startEntryId);
        int bookieIndex = 0;
        for (int i = 0; i < curEnsemble.size(); i++) {
            if (curEnsemble.get(i).equals(bookieSrc)) {
                bookieIndex = i;
                break;
            }
        }
        /*
         * Loop through all entries in the current ledger fragment range and
         * find the ones that were stored on the dead bookie.
         */
        List<Long> entriesToReplicate = new LinkedList<Long>();
        for (long i = startEntryId; i <= endEntryId; i++) {
            if (lh.getDistributionSchedule().getReplicaIndex(i, bookieIndex) >= 0) {
                /*
                 * Current entry is stored on the dead bookie so we'll need to
                 * read it and replicate it to a new bookie.
                 */
                entriesToReplicate.add(i);
            }
        }
        /*
         * Now asynchronously replicate all of the entries for the ledger
         * fragment that were on the dead bookie.
         */
        MultiCallback ledgerFragmentEntryMcb = new MultiCallback(entriesToReplicate.size(), ledgerFragmentMcb,
                null);
        for (final Long entryId : entriesToReplicate) {
            recoverLedgerFragmentEntry(entryId, lh, ledgerFragmentEntryMcb, newBookie);
        }
    }

    /**
     * This method asynchronously recovers a specific ledger entry by reading
     * the values via the BookKeeper Client (which would read it from the other
     * replicas) and then writing it to the chosen new bookie.
     * 
     * @param entryId
     *            Ledger Entry ID to recover.
     * @param lh
     *            LedgerHandle for the ledger
     * @param ledgerFragmentEntryMcb
     *            MultiCallback to invoke once we've recovered the current
     *            ledger entry.
     * @param newBookie
     *            New bookie we want to use to recover and replicate the ledger
     *            entries that were stored on the failed bookie.
     */
    private void recoverLedgerFragmentEntry(final Long entryId, final LedgerHandle lh,
            final MultiCallback ledgerFragmentEntryMcb, final InetSocketAddress newBookie) {
        /*
         * Read the ledger entry using the LedgerHandle. This will allow us to
         * read the entry from one of the other replicated bookies other than
         * the dead one.
         */
        lh.asyncReadEntries(entryId, entryId, new ReadCallback() {
            @Override
            public void readComplete(int rc, LedgerHandle lh, Enumeration<LedgerEntry> seq, Object ctx) {
                if (rc != Code.OK.intValue()) {
                    LOG.error("BK error reading ledger entry: " + entryId, BKException.create(rc));
                    ledgerFragmentEntryMcb.processResult(rc, null, null);
                    return;
                }
                /*
                 * Now that we've read the ledger entry, write it to the new
                 * bookie we've selected.
                 */
                LedgerEntry entry = seq.nextElement();
                ChannelBuffer toSend = lh.getDigestManager().computeDigestAndPackageForSending(entryId,
                        lh.getLastAddConfirmed(), entry.getLength(), entry.getEntry());
                bkc.getBookieClient().addEntry(newBookie, lh.getId(), lh.getLedgerKey(), entryId, toSend,
                        new WriteCallback() {
                            @Override
                            public void writeComplete(int rc, long ledgerId, long entryId, InetSocketAddress addr,
                                    Object ctx) {
                                if (rc != Code.OK.intValue()) {
                                    LOG.error("BK error writing entry for ledgerId: " + ledgerId + ", entryId: "
                                            + entryId + ", bookie: " + addr, BKException.create(rc));
                                } else {
                                    LOG.debug("Success writing ledger entry to a new bookie!");
                                }
                                /*
                                 * Pass the return code result up the chain with
                                 * the parent callback.
                                 */
                                ledgerFragmentEntryMcb.processResult(rc, null, null);
                            }
                        }, null);
            }
        }, null);
    }

    /**
     * Main method so we can invoke the bookie recovery via command line.
     * 
     * @param args
     *            Arguments to BookKeeperTools. 2 are required and the third is
     *            optional. The first is a comma separated list of ZK server
     *            host:port pairs. The second is the host:port socket address
     *            for the bookie we are trying to recover. The third is the
     *            host:port socket address of the optional destination bookie
     *            server we want to replicate the data over to.
     * @throws InterruptedException
     * @throws IOException
     * @throws KeeperException
     */
    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
        // Validate the inputs
        if (args.length < 2) {
            System.err.println("USAGE: BookKeeperTools zkServers bookieSrc [bookieDest]");
            return;
        }
        // Parse out the input arguments
        String zkServers = args[0];
        String bookieSrcString[] = args[1].split(COLON);
        if (bookieSrcString.length < 2) {
            System.err
                    .println("BookieSrc inputted has invalid name format (host:port expected): " + bookieSrcString);
            return;
        }
        final InetSocketAddress bookieSrc = new InetSocketAddress(bookieSrcString[0],
                Integer.parseInt(bookieSrcString[1]));
        InetSocketAddress bookieDest = null;
        if (args.length < 3) {
            String bookieDestString[] = args[2].split(COLON);
            if (bookieDestString.length < 2) {
                System.err.println(
                        "BookieDest inputted has invalid name format (host:port expected): " + bookieDestString);
                return;
            }
            bookieDest = new InetSocketAddress(bookieDestString[0], Integer.parseInt(bookieDestString[1]));
        }

        // Create the BookKeeperTools instance and perform the bookie recovery
        // synchronously.
        BookKeeperTools bkTools = new BookKeeperTools(zkServers);
        bkTools.recoverBookieData(bookieSrc, bookieDest);

        // Shutdown the resources used in the BookKeeperTools instance.
        bkTools.shutdown();
    }

}