Package net.citrusleaf

Source Code of net.citrusleaf.CitrusleafClient

/*
* Citrusleaf Aerospike Client - Java Library
*
* Copyright 2009-2010 by Citrusleaf, Inc. All rights reserved.
*
* Availability of this source code to partners and customers includes
* redistribution rights covered by individual contract. Please check your
* contract for exact rights and responsibilities.
*/

package net.citrusleaf;

import gnu.crypto.hash.HashFactory;
import gnu.crypto.hash.IMessageDigest;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.LockSupport;

/**
* Instantiate a <code>CitrusleafClient</code> object to access a Citrusleaf
* database cluster and perform database operations.
* <p>
* Your application uses this class API to perform database operations such as
* writing and reading records, and scanning sets of records. Write operations
* include specialized functionality such as append/prepend and arithmetic
* addition.
* <p>
* Records are stored and identified using a specified namespace, an optional
* set name, and a key which must be unique within a set.
* <p>
* Each record may have multiple bins, unless the Citrusleaf server nodes are
* configured as "single-bin". In "multi-bin" mode, partial records may be
* written or read by specifying the relevant subset of bins.
*/
public class CitrusleafClient {

  //=================================================================
  // Constants
  //

  /**
   * Database operation result codes.
   */
  public enum ClResultCode {
    /**
     * Operation was successful.
     */
    OK,

    /**
     * Initial "empty" value, operation has not started.
     */
    NOT_SET,

    /**
     * Unknown server failure.
     */
    SERVER_ERROR,

    /**
     * Operation timed out.
     */
    TIMEOUT,

    /**
     * Memory or other client fault.
     */
    CLIENT_ERROR,

    /**
     * On retrieving, touching or replacing a record that doesn't exist.
     */
    KEY_NOT_FOUND_ERROR,

    /**
     * On modifying a record with unexpected generation.
     */
    GENERATION_ERROR,

    /**
     * Bad parameter(s) were passed in database operation call.
     */
    PARAMETER_ERROR,

    /**
     * On create-only (write unique) operations on a record that already
     * exists.
     */
    KEY_EXISTS_ERROR,

    /**
     * On create-only (write unique) operations on a bin that already
     * exists.
     */
    BIN_EXISTS_ERROR,

    /**
     * Object could not be serialized.
     */
    SERIALIZE_ERROR,

    /**
     * Expected cluster ID was not received.
     */
    CLUSTER_KEY_MISMATCH,

    /**
     * Server has run out of memory.
     */
    SERVER_MEM_ERROR,

    /**
     * XDS product is not available.
     */
    NO_XDS,

    /**
     * Server is not accepting requests.
     */
    SERVER_NOT_AVAILABLE,

    /**
     * Operation is not supported with configured bin type (single-bin or
     * multi-bin).
     */
    BIN_TYPE_ERROR,

    /**
     * Record size exceeds limit.
     */
    RECORD_TOO_BIG,

    /**
     * Too many concurrent operations on the same record.
     */
    KEY_BUSY
  }

  /**
   * Priority of scan operations on database server.
   */
  public enum ClScanningPriority {
    /**
     * A server node autonomously decides how many scan threads to use based
     * on factors such as the number of storage devices.
     */
    AUTO,

    /**
     * Currently this means a server node will use one scan thread.
     */
    LOW,

    /**
     * Currently this means a server node will use three scan threads.
     */
    MEDIUM,

    /**
     * Currently this means a server node will use five scan threads.
     */
    HIGH
  }

  /**
   * Log escalation level.
   */
  public enum ClLogLevel {
    /**
     * Error condition has occurred.
     */
    ERROR,

    /**
     * Unusual non-error condition has occurred.
     */
    WARN,

    /**
     * Normal information message.
     */
    INFO,

    /**
     * Message used for debugging purposes.
     */
    DEBUG,

    /**
     * Detailed message also used for debugging purposes.
     */
    VERBOSE
  }

  // Package-Private
  static final int DEFAULT_TIMEOUT = 5000;

  private enum PendType {
    PREPEND,
    APPEND
  }

  private enum RetryPolicy {
    ONE_SHOT,
    RETRY
  }

  private static final int MAX_TIMEOUT_MS_WAIT = 100;
  private static final int MIN_TIMEOUT_MS_WAIT = 1;
  private static final int RETRY_COUNT = 3;

  private static final String DEFAULT_NAMESPACE = "ns";


  //=================================================================
  // Globals
  //

  private static SimpleDateFormat gSdf = null;
  private static ClLogLevel gLogLevel = ClLogLevel.INFO;
  private static LogCallback gLogCallback = null;
  private static ConcurrentHashMap<String,CLConnectionManager> gConnMgrMap =
      new ConcurrentHashMap<String,CLConnectionManager>();


  //=================================================================
  // Member Data
  //

  private CLConnectionManager mConnMgr = null;
  private String mDefaultNamespace = DEFAULT_NAMESPACE;


  //=================================================================
  // Public Functions
  //

  //-------------------------------------------------------
  // Constructors
  //-------------------------------------------------------

  /**
   * Constructor, creates <code>CitrusleafClient</code> object and initializes
   * logging framework.
   * <p>
   * This constructor does not add hosts.
   */
  public CitrusleafClient() {
    ClLogInit();
  }

  /**
   * Constructor, combines default constructor {@link #CitrusleafClient()} and
   * {@link #addHost(String, int) addHost()}.
   *
   * @param hostname      host name
   * @param port        host port
   */
  public CitrusleafClient(String hostname, int port) {
    ClLogInit();
    addHost(hostname, port);
  }

  //-------------------------------------------------------
  // Do Logging via Callback
  //-------------------------------------------------------

  /**
   * Set logging level filter and optional log callback implementation.
   * <p>
   * This method may only be called once, at startup.
   *
   * @param level        only show logs at this or more urgent level
   * @param logCb        {@link LogCallback} implementation, pass
   *               <code>null</code> to let Citrusleaf client write
   *               logs to <code>System.err</code>
   */
  public static void setLogging(ClLogLevel level, LogCallback logCb) {
    gLogLevel = level;
    gLogCallback = logCb;
  }

  //-------------------------------------------------------
  // Cluster Connection Management
  //-------------------------------------------------------

  /**
   * Add a server database host to the client's cluster map.
   * <p>
   * The following actions occur upon the first invocation of this method:
   * <p>
   * - create new cluster map <br>
   * - add host to cluster map <br>
   * - connect to host server <br>
   * - request host's list of other nodes in cluster <br>
   * - add these nodes to cluster map <br>
   * <p>
   * Further invocations will add hosts to the cluster map if they don't
   * already exist. In most cases, only one <code>addHost()</code> call is
   * necessary. When this call succeeds, the client is ready to process
   * database requests.
   *
   * @param hostname      host name
   * @param port        host port
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode addHost(String hostname, int port) {
    // If cluster has been initialized, add host to cluster.
    if (mConnMgr != null) {
      return mConnMgr.addHost(hostname, port);
    }

    // Lookup host in global cluster map, return if host already exists.
    String key = String.format("%s:%d", hostname, port);

    mConnMgr = gConnMgrMap.get(key);

    if (mConnMgr != null) {
      return ClResultCode.OK;
    }

    // Create new cluster.
    try {
      mConnMgr = new CLConnectionManager(hostname, port, 200);
    }
    catch (Exception ex) {
      // Failed to establish connection to host, bail on cluster creation.
      warn(ex.getMessage());
      return ClResultCode.SERVER_NOT_AVAILABLE;
    }

    // Check to see if another thread created cluster first.
    CLConnectionManager prevConnMgr =
        gConnMgrMap.putIfAbsent(key, mConnMgr);

    if (prevConnMgr != null) {
      // Don't use the one we allocated, use the one from the map.
      mConnMgr.close();
      mConnMgr = prevConnMgr;
    }

    return ClResultCode.OK;
  }

  /**
   * Determine if we are ready to talk to the database server cluster.
   *
   * @return          <code>true</code> if cluster is ready,
   *               <code>false</code> if not
   */
  public boolean isConnected() {
    return mConnMgr != null ? mConnMgr.connected() : false;
  }

  /**
   * Use {@link #isConnected()}.
   */
  @Deprecated
  public boolean connect() {
    return isConnected();
  }

  /**
   * Close client connections to database server nodes.
   */
  public void close() {
    // Some may linger if they haven't responded or timed out.
    mConnMgr.closeAllConnections();
  }

  /**
   * Specify namespace to use in database operation calls that do not have a
   * namespace parameter.
   *
   * @param namespace      namespace to use in calls that have no namepsace
   *               parameter
   */
  public void setDefaultNamespace(String namespace) {
    mDefaultNamespace = namespace;
  }

  //-------------------------------------------------------
  // Key Digest Computation
  //-------------------------------------------------------

  /**
   * Generate digest from key and set name.
   *
   * @param set        set name
   * @param key        record identifier, unique within set
   * @return          unique identifier generated from key and set
   *               name
   */
  public static byte[] computeDigest(String set, Object key) {
    CLBuffer cb = new CLBuffer();

    return cb.getDigest(set, key);
  }

  //-------------------------------------------------------
  // Set Operations
  //-------------------------------------------------------

  /**
   * For server nodes configured as "single-bin" only - set record value for
   * specified key, using default namespace, no set name, and default options.
   * <p>
   * If the record does not exist, it will be created. If it exists its value
   * is replaced.
   *
   * @param key        record identifier, unique within set
   * @param value        single-bin value
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode set(Object key, Object value) {
    return set(key, value, null, null);
  }

  /**
   * For server nodes configured as "single-bin" only - set record value for
   * specified key, using default namespace and no set name.
   * <p>
   * If the record does not exist, it will be created. If it exists its value
   * is replaced.
   *
   * @param key        record identifier, unique within set
   * @param value        single-bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode set(Object key, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return set(mDefaultNamespace, "", key, binAsCollection("", value),
        opts, wOpts);
  }

  /**
   * Set record bin value for specified key and bin name.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode set(String namespace, String set, Object key,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return set(namespace, set, key, binAsCollection(binName, value), opts,
        wOpts);
  }

  /**
   * Set record bin values for specified key and bin names.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode set(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("set", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addWrite(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  /**
   * Set record bin values for specified key and bin names.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode set(String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("set", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addWrite(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Set Operations - by digest
  //-------------------------------------------------------

  /**
   * Set record bin value for specified key digest and bin name.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode setDigest(String namespace, byte[] digest,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return setDigest(namespace, digest, binAsMap(binName, value), opts,
        wOpts);
  }

  /**
   * Set record bin value for specified key digest and bin name.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bin        bin name/value pair
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode setDigest(String namespace, byte[] digest, ClBin bin,
      ClOptions opts, ClWriteOptions wOpts) {
    return setDigest(namespace, digest, binAsMap(bin.name, bin.value), opts,
        wOpts);
  }

  /**
   * Set record bin values for specified key digest and bin names.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode setDigest(String namespace, byte[] digest,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    HashMap<String, Object> binsAsMap = new HashMap<String, Object>();

    for (ClBin bin: bins) {
      binsAsMap.put(bin.name, bin.value);
    }

    return setDigest(namespace, digest, binsAsMap, opts, wOpts);
  }

  /**
   * Set record bin values for specified key digest and bin names.
   * <p>
   * If the record or bin does not exist, it will be created. If the bin
   * exists its value is replaced. Others bins are ignored.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode setDigest(String namespace, byte[] digest,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("setDigest", namespace, digest, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, digest, wOpts);
    asb.addWrite(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Append & Prepend Operations
  //-------------------------------------------------------

  /**
   * For server nodes configured as "single-bin" only - append value to
   * existing record value for specified key, using default namespace, no set
   * name, and default options.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param key        record identifier, unique within set
   * @param value        value to append
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode append(Object key, Object value) {
    return pend(PendType.APPEND, key, value);
  }

  /**
   * For server nodes configured as "single-bin" only - append value to
   * existing record value for specified key, using default namespace and no
   * set name.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param key        record identifier, unique within set
   * @param value        value to append
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode append(Object key, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return pend(PendType.APPEND, key, value, opts, wOpts);
  }

  /**
   * Append value to existing bin value for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        value to append
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode append(String namespace, String set, Object key,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return pend(PendType.APPEND, namespace, set, key, binName, value, opts,
        wOpts);
  }

  /**
   * Append value to existing bin value for each bin for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode append(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    return pend(PendType.APPEND, namespace, set, key, bins, opts, wOpts);
  }

  /**
   * Append value to existing bin value for each bin for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode append(String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    return pend(PendType.APPEND, namespace, set, key, bins, opts, wOpts);
  }

  /**
   * For server nodes configured as "single-bin" only - prepend value to
   * existing record value for specified key, using default namespace, no set
   * name, and default options.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param key        record identifier, unique within set
   * @param value        value to prepend
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode prepend(Object key, Object value) {
    return pend(PendType.PREPEND, key, value);
  }

  /**
   * For server nodes configured as "single-bin" only - prepend value to
   * existing record value for specified key, using default namespace and no
   * set name.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param key        record identifier, unique within set
   * @param value        value to prepend
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode prepend(Object key, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return pend(PendType.PREPEND, key, value, opts, wOpts);
  }

  /**
   * Prepend value to existing bin value for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        value to prepend
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode prepend(String namespace, String set, Object key,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return pend(PendType.PREPEND, namespace, set, key, binName, value, opts,
        wOpts);
  }

  /**
   * Prepend value to existing bin value for each bin for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode prepend(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    return pend(PendType.PREPEND, namespace, set, key, bins, opts, wOpts);
  }

  /**
   * Prepend value to existing bin value for each bin for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * This call works only if the specified value's type matches the existing
   * value's type, and the type is not an integer (i.e. <code>Integer</code>
   * or <code>Long</code>.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode prepend( String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    return pend(PendType.PREPEND, namespace, set, key, bins, opts, wOpts);
  }

  //-------------------------------------------------------
  // Arithmetic Operations
  //-------------------------------------------------------

  /**
   * For integer values, and server nodes configured as "single-bin" only -
   * add value to existing record value for specified key, using default
   * namespace, no set name, and default options.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   *
   * @param key        record identifier, unique within set
   * @param value        bin value
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode add(Object key, Object value) {
    return add(key, value, null, null);
  }

  /**
   * For integer values, and server nodes configured as "single-bin" only -
   * add value to existing record value for specified key, using default
   * namespace and no set name.
   * <p>
   * If the record does not exist, it will be created with the specified
   * value.
   *
   * @param key        record identifier, unique within set
   * @param value        bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode add(Object key, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return add(mDefaultNamespace, "", key, binAsCollection("", value), opts,
        wOpts);
  }

  /**
   * For integer values only - add value to existing bin value for specified
   * key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode add(String namespace, String set, Object key,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return add(namespace, set, key, binAsCollection(binName, value), opts,
        wOpts);
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode add(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("add", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAdd(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode add(String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("add", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAdd(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key, and return the results.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   *               with new values
   */
  public ClResult addAndGet(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("addAndGet", namespace, set, key, bins)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAdd(bins);

    ArrayList<String> binNames = new ArrayList<String>(bins.size());

    for (ClBin bin : bins) {
      binNames.add(bin.name);
    }

    asb.addRead(binNames);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key, and return the results.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   *               with new values
   */
  public ClResult addAndGet(String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("addAndGet", namespace, set, key, bins)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAdd(bins);

    ArrayList<String> binNames = new ArrayList<String>(bins.size());

    for (String binName : bins.keySet()) {
      binNames.add(binName);
    }

    asb.addRead(binNames);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  //-------------------------------------------------------
  // Arithmetic Operations - by digest
  //-------------------------------------------------------

  /**
   * For integer values only - add value to existing bin value for specified
   * key digest.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param binName      bin name, pass <code>""</code> if server nodes
   *               are configured as "single-bin"
   * @param value        bin value
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode addDigest(String namespace, byte[] digest,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return addDigest(namespace, digest, binAsMap(binName, value), opts,
        wOpts);
  }

  /**
   * For integer values only - add value to existing bin value for specified
   * key digest.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bin        bin name/value pair
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode addDigest(String namespace, byte[] digest, ClBin bin,
      ClOptions opts, ClWriteOptions wOpts) {
    return addDigest(namespace, digest, binAsMap(bin.name, bin.value), opts,
        wOpts);
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key digest.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bins        bin name/value pairs as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode addDigest(String namespace, byte[] digest,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    HashMap<String, Object> binsAsMap = new HashMap<String, Object>();

    for (ClBin bin: bins) {
      binsAsMap.put(bin.name, bin.value);
    }

    return addDigest(namespace, digest, binsAsMap, opts, wOpts);
  }

  /**
   * For integer values only - add value to existing bin value for each bin
   * for specified key digest.
   * <p>
   * If the record or bin does not exist, it will be created with the
   * specified value.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param bins        bin name/value pairs as <code>Map</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode addDigest(String namespace, byte[] digest,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("addDigest", namespace, digest, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, digest, wOpts);
    asb.addAdd(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Delete Operations
  //-------------------------------------------------------

  /**
   * Delete record for specified key, using default namespace and no set name.
   *
   * @param key        record identifier, unique within set
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode delete(Object key, ClOptions opts,
      ClWriteOptions wOpts) {
    return delete(mDefaultNamespace, "", key, opts, wOpts);
  }

  /**
   * Delete record for specified key.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode delete(String namespace, String set, Object key,
      ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("delete", namespace, set, key)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.setDelete();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Delete Operations - by digest
  //-------------------------------------------------------

  /**
   * Delete record for specified key digest.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode deleteDigest(String namespace, byte[] digest,
      ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("deleteDigest", namespace, digest)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, digest, wOpts);
    asb.setDelete();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Get Operations
  //-------------------------------------------------------

  /**
   * For server nodes configured as "single-bin" only - get record value for
   * specified key, using default namespace, no set name, and default options.
   *
   * @param key        record identifier, unique within set
   * @return          {@link ClResult} containing single-bin value
   */
  public Object get(Object key) {
    ClResult r =
        get(mDefaultNamespace, "", key, binNameAsCollection(""), null);

    // Single-bin result is in results Map, also put it in result Object:
    return r != null && r.results != null ? r.results.get("") : null;
  }

  /**
   * Get bin value for specified key and bin name.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binName      bin name filter, pass <code>""</code> if server
   *               nodes are configured as "single-bin"
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin value
   */
  public ClResult get(String namespace, String set, Object key,
      String binName, ClOptions opts) {
    ClResult r =
        get(namespace, set, key, binNameAsCollection(binName), opts);

    // Result for one bin is in results Map, also put it in result Object:
    if (r != null && r.results != null) {
      r.result = r.results.values().toArray()[0];
    }

    return r;
  }

  /**
   * Get bin name/value pairs for specified key and list of bin names.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binNames      bin names filter as array
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult get(String namespace, String set, Object key,
      String[] binNames, ClOptions opts) {
    return get(namespace, set, key, Arrays.asList(binNames), opts);
  }

  /**
   * Get bin name/value pairs for specified key and list of bin names.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binNames      bin names filter as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult get(String namespace, String set, Object key,
      Collection<String> binNames, ClOptions opts) {
    if (! isValidArgs("get", namespace, set, key, binNames)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, null);
    asb.addRead(binNames);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  /**
   * Get all bin name/value pairs for specified key.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult getAll(String namespace, String set, Object key,
      ClOptions opts) {
    if (! isValidArgs("getAll", namespace, set, key)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, null);
    asb.readAll();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
 

  /**
   * Get bin name/value pairs for specified key and list of bin names, and if
   * the record exists, reset record time to expiration.
   * <p>
   * This operation will increment the record generation.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param binNames      bin names filter as <code>Collection</code>
   * @param expiration    new record expiration (see
   *               {@link ClWriteOptions#expiration})
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult getWithTouch(String namespace, String set, Object key,
      Collection<String> binNames, int expiration, ClOptions opts) {
    if (! isValidArgs("getWithTouch", namespace, set, key, binNames)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    ClWriteOptions wOpts = new ClWriteOptions();

    wOpts.expiration = expiration;

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addRead(binNames);
    asb.addTouch();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  /**
   * Batch multiple <code>get()</code> requests into a single call.
   * <p>
   * Elements of <code>keys</code> are positionally matched with elements of
   * result array.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param keys        batch of keys as <code>Collection</code>
   * @param binName      bin name filter, pass <code>""</code> if server
   *               nodes are configured as "single-bin"
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult result} array where each element
   *               contains bin name/value pairs
   */
  public ClResult[] batchGet(String namespace, String set,
      Collection<Object> keys, String binName, ClOptions opts) {
    return batchGetGeneric(namespace, set, keys,
        binNameAsCollection(binName), opts, false, true);
  }

  /**
   * Batch multiple <code>get()</code> requests into a single call.
   * <p>
   * Elements of <code>keys</code> are positionally matched with elements of
   * result array.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param keys        batch of keys as <code>Collection</code>
   * @param binNames      bin names filter as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult result} array where each element
   *               contains bin name/value pairs
   */
  public ClResult[] batchGet(String namespace, String set,
      Collection<Object> keys, Collection<String> binNames,
      ClOptions opts) {
    return batchGetGeneric(namespace, set, keys, binNames, opts, false,
        true);
  }

  /**
   * Batch multiple <code>getAll()</code> requests into a single call.
   * <p>
   * Elements of <code>keys</code> are positionally matched with elements of
   * result array.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param keys        batch of keys as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult result} array where each element
   *               contains bin name/value pairs
   */
  public ClResult[] batchGetAll(String namespace, String set,
      Collection<Object> keys, ClOptions opts) {
    return batchGetGeneric(namespace, set, keys, null, opts, true, true);
  }

  //-------------------------------------------------------
  // Get Operations - by digest
  //-------------------------------------------------------

  /**
   * Get bin value for specified key digest and bin name.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param binName      bin name filter, pass <code>""</code> if server
   *               nodes are configured as "single-bin"
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin value
   */
  public ClResult getDigest(String namespace, byte[] digest, String binName,
      ClOptions opts) {
    return getDigest(namespace, digest, binNameAsCollection(binName), opts);
  }

  /**
   * Get bin name/value pairs for specified key digest and list of bin names.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param binNames      bin names filter as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult getDigest(String namespace, byte[] digest,
      Collection<String> binNames, ClOptions opts) {
    if (! isValidArgs("getDigest", namespace, digest, binNames)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, digest, null);
    asb.addRead(binNames);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  /**
   * Get all bin name/value pairs for specified key digest.
   * <p>
   * Non-Digest database methods send the normal key and set name to the
   * server which converts them to a digest. Digest methods send the digest
   * directly.
   *
   * @param namespace      namespace
   * @param digest      unique identifier generated from key and set
   *               name
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} containing bin name/value pairs
   */
  public ClResult getAllDigest(String namespace, byte[] digest,
      ClOptions opts) {
    if (! isValidArgs("getAllDigest", namespace, digest)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, digest, null);
    asb.readAll();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  //-------------------------------------------------------
  // Existence-Check Operations
  //-------------------------------------------------------

  /**
   * Check if specified key exists, using default namespace, no set name, and
   * default options.
   *
   * @param key        record identifier, unique within set
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode exists(Object key) {
    return exists(mDefaultNamespace, "", key, null);
  }

  /**
   * Check if specified key exists.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode exists(String namespace, String set, Object key,
      ClOptions opts) {
    if (! isValidArgs("exists", namespace, set, key)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, null);
    asb.readExists();
    asb.readNoBindata();

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  // TODO - verify result format
  /**
   * Batch multiple <code>exists()</code> requests into a single call.
   * <p>
   * Elements of <code>keys</code> are positionally matched with elements of
   * result array.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param keys        batch of keys as <code>Collection</code>
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult result} array where each element
   *               indicates whether its corresponding key exists
   */
  public ClResult[] batchExists(String namespace, String set,
      Collection<Object> keys, ClOptions opts) {
    return batchGetGeneric(namespace, set, keys, null, opts, false, false);
  }

  //-------------------------------------------------------
  // Scan Operations
  //-------------------------------------------------------

  /**
   * Scan the database, retrieving all records in specified namespace.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResult} for final record
   */
  @Deprecated
  public ClResult scan(String namespace, ClOptions opts, ScanCallback scanCb,
      Object userData) {
    return scan(namespace, null, opts, scanCb, userData, false);
  }

  /**
   * Scan the database, retrieving all records in specified namespace.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, not supported as scan filter
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResult} for final record
   */
  @Deprecated
  public ClResult scan(String namespace, String set, ClOptions opts,
      ScanCallback scanCb, Object userData) {
    return scan(namespace, set, opts, scanCb, userData, false);
  }

  /**
   * Scan the database using specified namespace filter, retrieving records or
   * digests only, as specified.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResult} for final record
   */
  @Deprecated
  public ClResult scan(String namespace, ClOptions opts, ScanCallback scanCb,
      Object userData, boolean noBinData) {
    return scan(namespace, null, opts, scanCb, userData, noBinData);
  }

  /**
   * Scan the database using specified namespace filter, retrieving records or
   * digests only, as specified.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, not supported as scan filter
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @param noBinData      <code>true</code> to retrieve digests only,
   *               <code>false</code> to retrieve bin data
   * @return          {@link ClResult} for final record
   */
  @Deprecated
  public ClResult scan(String namespace, String set, ClOptions opts,
      ScanCallback scanCb, Object userData, boolean noBinData) {
    if (opts == null) {
      opts = new ClOptions(DEFAULT_TIMEOUT);
      opts.setOneShot();
    }
    else {
      if (opts.mRetryPolicy != RetryPolicy.ONE_SHOT) {
        warn("scan: must not have retry set");
        return new ClResult(ClResultCode.CLIENT_ERROR);
      }
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, null, null);
    asb.readIterate(scanCb, userData);

    if (noBinData) {
      asb.readNoBindata();
    }

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  /**
   * Scan a single database node, using specified namespace and set filters,
   * retrieving records or digests only, as specified.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param nodeName      node to scan
   * @param namespace      namespace
   * @param set        set name, pass <code>null</code> to retrieve all
   *               records in the namespace
   * @param noBinData      <code>true</code> to retrieve digests only,
   *               <code>false</code> to retrieve bin data
   * @param scanPercent    fraction of data to scan - not yet supported
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanOpts      parallel scan policy attributes, pass
   *               <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResultCode result status}
   */
  public ClResultCode scanNode(String nodeName, String namespace, String set,
      boolean noBinData, int scanPercent, ClOptions opts,
      ClScanningOptions scanOpts, ScanCallback scanCb, Object userData) {
    if (scanOpts == null) {
      scanOpts = new ClScanningOptions();
    }

    if (scanPercent > 100 || scanPercent < 0) {
      warn("invalid scan percent");
      return ClResultCode.PARAMETER_ERROR;
    }

    if (opts == null) {
      opts = new ClOptions(DEFAULT_TIMEOUT);
      opts.setOneShot();
    }
    else if (opts.mRetryPolicy != RetryPolicy.ONE_SHOT) {
      warn("scan must be one-shot");
      return ClResultCode.PARAMETER_ERROR;
    }

    CLNode node = mConnMgr.getNodeFromNodeName(nodeName);

    if (node == null) {
      warn("cannot start scanning node " + nodeName +
          " - does not exist (did it die?)");
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, null, null);
    asb.readIterate(scanCb, userData);

    if (noBinData) {
      asb.readNoBindata();
    }

    asb.scan_options(scanOpts, scanPercent);

    try {
      asb.prepare();
    }
    catch (SerializeException e) {
      asb.destroy();
      return ClResultCode.SERIALIZE_ERROR;
    }

    ClResult r = doRequestUsingNode(asb, node, opts);

    asb.destroy();
    return r.resultCode;
  }

  /**
   * Scan the database nodes in parallel, using specified namespace and set
   * filters, and default options, returning a single result code.
   * <p>
   * This call is the same as
   * {@link #scanAllNodes(String, String, ScanCallback, Object)
   * scanAllNodes()} but returns only one result code.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, pass <code>null</code> to retrieve all
   *               records in the namespace
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResultCode result status},
   *               {@link ClResultCode#OK OK} if all nodes
   *               succeeded, else first error code encountered
   */
  public ClResultCode scan(String namespace, String set, ScanCallback scanCb,
      Object userData) {
    Map<String, ClResultCode> resultMap = scanAllNodes(namespace, set,
        false, 100, null, null, scanCb, userData);

    for (ClResultCode rc : resultMap.values()) {
      if (rc != ClResultCode.OK) {
        return rc;
      }
    }

    return ClResultCode.OK;
  }

  /**
   * Scan the database nodes in parallel, using specified namespace and set
   * filters, and default options.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, pass <code>null</code> to retrieve all
   *               records in the namespace
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResultCode result status} for each node
   */
  public Map<String, ClResultCode> scanAllNodes(String namespace, String set,
      ScanCallback scanCb, Object userData) {
    return scanAllNodes(namespace, set, false, 100, null, null, scanCb,
        userData);
  }

  /**
   * Scan the database nodes in parallel, using specified namespace and set
   * filters.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, pass <code>null</code> to retrieve all
   *               records in the namespace
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanOpts      parallel scan policy attributes, pass
   *               <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResultCode result status} for each node
   */
  public Map<String, ClResultCode> scanAllNodes(String namespace, String set,
      ClOptions opts, ClScanningOptions scanOpts, ScanCallback scanCb,
      Object userData) {
    return scanAllNodes(namespace, set, false, 100, opts, scanOpts, scanCb,
        userData);   
  }

  /**
   * Scan the database nodes in parallel, using specified namespace and set
   * filters, retrieving records or digests only, as specified.
   * <p>
   * This call will block until the scan is complete - callbacks are made
   * within the scope of this call.
   *
   * @param namespace      namespace
   * @param set        set name, pass <code>null</code> to retrieve all
   *               records in the namespace
   * @param noBinData      <code>true</code> to retrieve digests only,
   *               <code>false</code> to retrieve bin data
   * @param scanPercent    fraction of data to scan - not yet supported
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param scanOpts      parallel scan policy attributes, pass
   *               <code>null</code> to use defaults
   * @param scanCb      {@link ScanCallback} implementation
   * @param userData      pass-through user-defined data
   * @return          {@link ClResultCode result status} for each node
   */
  public Map<String, ClResultCode> scanAllNodes(String namespace, String set,
      boolean noBinData, int scanPercent, ClOptions opts,
      ClScanningOptions scanOpts, ScanCallback scanCb, Object userData) {
    if (scanOpts == null) {
      scanOpts = new ClScanningOptions();
    }

    if (scanPercent > 100 || scanPercent < 0) {
      warn("invalid scan percent");
      return null;
    }

    if (opts == null) {
      opts = new ClOptions(DEFAULT_TIMEOUT);
      opts.setOneShot();
    }
    else if (opts.mRetryPolicy != RetryPolicy.ONE_SHOT) {
      warn("scan must be one-shot");
      return null;
    }

    // Figure out how many nodes we should go to.
    List<String> nodeNames = mConnMgr.getNodeNameList();

    if (nodeNames == null) {
      warn("no node names in cluster");
      return null;
    }

    // Initialize all the results to be NOT_SET.
    Map<String, ClResultCode> resultMap =
        new HashMap<String, ClResultCode>(nodeNames.size());

    for (String nodeName : nodeNames) {
      resultMap.put(nodeName, ClResultCode.NOT_SET);
    }

    // Do the job.
    if (scanOpts.isConcurrentNodes()) {
      debug("starting concurrent node scan for " + namespace + " " +
          resultMap.size() + " node(s)");

      List<CLScanNodeThread> nodeThreads =
          new ArrayList<CLScanNodeThread>(nodeNames.size());

      for (String nodeName : nodeNames) {
        CLScanNodeThread t = new CLScanNodeThread(this, nodeName,
            namespace, set, noBinData, scanPercent, scanCb, opts,
            scanOpts, userData);

        nodeThreads.add(t);
        t.start();
      }

      try {
        for (Thread t : nodeThreads) {
          t.join();
        }

        for (CLScanNodeThread t : nodeThreads) {
          resultMap.put(t.getNodeName(), t.getResult());
        }
      }
      catch (InterruptedException e) {
        info("interrupted");
      }
    }
    else {
      debug("starting serial node scan for " + namespace + " " +
          resultMap.size() + " node(s)");

      for (String nodeName : nodeNames) {
        ClResultCode rc = scanNode(nodeName, namespace, set, noBinData,
            scanPercent, opts, scanOpts, scanCb, userData);

        resultMap.put(nodeName, rc);
      }
    }

    return resultMap;
  }

  //-------------------------------------------------------
  // Low-level Operations
  //-------------------------------------------------------

  /**
   * Combine <code>get()</code> and <code>set()</code> operations for a
   * specified record.
   *
   * @param namespace      namespace
   * @param set        optional set name
   * @param key        record identifier, unique within set
   * @param readBinNames    bin names filter for read
   * @param writeBins      bin name/value pairs to write
   * @param opts        {@link ClOptions transaction policy attributes},
   *               pass <code>null</code> to use defaults
   * @param wOpts        {@link ClWriteOptions write policy attributes},
   *               pass <code>null</code> to use defaults
   * @return          {@link ClResult} for read, containing bin
   *               name/value pairs
   */
  public ClResult operate(String namespace, String set, Object key,
      Collection<String> readBinNames, Collection<ClBin> writeBins,
      ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("operate", namespace, set, key, readBinNames,
        writeBins)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, null);

    if (readBinNames != null) {
      asb.addRead(readBinNames);
    }

    if (writeBins != null) {
      asb.addWrite(writeBins);
    }

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r;
  }

  //-------------------------------------------------------
  // RTA Operations
  //-------------------------------------------------------

  // TODO - is there a way to hide specific public functions in javadoc ???
  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegment(Object key, Object value) {
    return appendSegment(key, value, null, null);
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegment(Object key, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return appendSegment(mDefaultNamespace, "", key,
        binAsCollection("", value), opts, wOpts);
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegment(String namespace, String set, Object key,
      String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return appendSegment(namespace, set, key,
        binAsCollection(binName, value), opts, wOpts);
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegment(String namespace, String set, Object key,
      Collection<ClBin> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("appendSegment", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAppendSegment(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegment(String namespace, String set, Object key,
      Map<String, Object> bins, ClOptions opts, ClWriteOptions wOpts) {
    if (! isValidArgs("appendSegment", namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAppendSegment(bins);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
 

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResultCode appendSegmentAs(String namespace, String set,
      Object key, String binName, Object value,
      ClAppendSegmentWriteOptions appendSegmentOpts, ClOptions opts,
      ClWriteOptions wOpts) {
    if (! isValidArgs("appendSegmentAs", namespace, set, key, value)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);
    asb.addAppendSegmentExt(binName, value, appendSegmentOpts);

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public ClResult getUsing(String namespace, String set, Object key,
      String binName, ClOptions opts,
      Collection<ClAppendSegmentQueryFilter> filters, int maxResults) {
    if (! isValidArgs("getUsing", namespace, set, key)) {
      return new ClResult(ClResultCode.PARAMETER_ERROR);
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, null);
    asb.addAppendSegmentQuery(binName, filters, maxResults);

    ClResult r = doRequest(asb, opts);

    if (r != null && r.results != null) {
      r.result = r.results.values().toArray()[0];
    }

    asb.destroy();

    return r;
  }


  //=================================================================
  // Public Nested Interfaces & Classes
  //

  //-------------------------------------------------------
  // Callbacks
  //-------------------------------------------------------

  /**
   * An object implementing this interface may be passed in
   * <code>setLogging()</code>, so the caller can channel Citrusleaf client
   * logs as desired.
   */
  public static interface LogCallback {
    /**
     * This method will be called for each Citrusleaf client log statement.
     *
     * @param level        {@link ClLogLevel log level}
     * @param msg        log message
     */
    public void logCallback(ClLogLevel level, String msg);
  }

  /**
   * An object implementing this interface is passed in <code>scan...()</code>
   * calls, so the caller can be notified with scan results.
   */
  public static interface ScanCallback {
    /**
     * This method will be called for each record returned from a scan.
     *
     * @param namespace      namespace
     * @param set        set name
     * @param digest      unique ID generated from key and set name
     * @param bins        bin name/value pairs as <code>Map</code>
     * @param generation    how many times the record has been modified
     * @param expirationDate  date this record will expire, in seconds
     *               from Jan 01 2010 00:00:00 GMT
     * @param userData      pass-through user-defined data
     */
    public void scanCallback(String namespace, String set, byte[] digest,
        Map<String, Object> bins, int generation, int expirationDate,
        Object userData);
  }

  //-------------------------------------------------------
  // Transaction Options
  //-------------------------------------------------------

  /**
   * Container object for transaction policy attributes used in all database
   * operation calls.
   * <p>
   * This object is passed in all database operation calls to specify options.
   * <code>null</code> may be passed to use defaults for all options.
   */
  public static class ClOptions {
    // Package-Private
    int mTimeout;
    RetryPolicy mRetryPolicy;

    // TODO - is there a way to show a named constant's value in javadoc ???
    /**
     * Constructor, sets default options.
     * <p>
     * The object is constructed with timeout set to <code>5000</code>
     * milliseconds and default transaction retry policy.
     */
    public ClOptions() {
      mTimeout = DEFAULT_TIMEOUT;
      mRetryPolicy = RetryPolicy.RETRY;
    }

    /**
     * Constructor, specifying timeout.
     * <p>
     * The object is constructed with specified timeout and default
     * transaction retry policy.
     *
     * @param timeoutMillisec  transaction duration limit, milliseconds
     */
    public ClOptions(int timeoutMillisec) {
      mTimeout = timeoutMillisec;
      mRetryPolicy = RetryPolicy.RETRY;
    }

    /**
     * Specify no transaction retries.
     */
    public void setOneShot() {
      mRetryPolicy = RetryPolicy.ONE_SHOT;
    }

    /**
     * Specify timeout.
     *
     * @param timeoutMillisec  transaction duration limit, milliseconds
     */
    public void setTimeout(int timeoutMillisec) {
      mTimeout = timeoutMillisec;
    }

    // Package-Private
    static boolean wantsRetry(ClOptions opts) {
      return opts != null && opts.mRetryPolicy == RetryPolicy.ONE_SHOT ?
          false : true;
    }
  }

  /**
   * Container object for policy attributes used in write operations.
   * <p>
   * This object is passed in <code>set()</code>, <code>append()</code>,
   * <code>prepend()</code>, <code>add()</code>, and <code>delete()</code>
   * calls to specify write options. <code>null</code> may be passed to use
   * defaults for all options.
   * <p>
   * Set <code>expiration</code> and <code>unique</code> directly, and call
   * <code>set_generation...()</code> methods to override defaults.
   * <p>
   * Generation is the number of times a record has been modified (including
   * creation) on the server. Therefore if a write operation is creating a
   * record, the expected generation would be <code>0</code>.
   */
  public static class ClWriteOptions {
    /**
     * Record is automatically removed from server this many seconds after
     * write operation.
     */
    public int expiration;

    /**
     * Write operation is create-only, will fail if record already exists.
     */
    public boolean unique;

    // Package-Private
    boolean mUseGeneration;
    boolean mUseGenerationGt;
    boolean mUseGenerationDup;
    int mGeneration;

    /**
     * Constructor, sets default write options.
     * <p>
     * The object is constructed with <code>expiration</code> of
     * <code>0</code>, <code>unique</code> set <code>false</code>, and no
     * expected generation.
     */
    public ClWriteOptions() {
      expiration = 0;
      unique = false;

      mUseGeneration = false;
      mUseGenerationGt = false;
      mUseGenerationDup = false;
      mGeneration = 0;
    }

    /**
     * Specify expected generation.
     * <p>
     * Write operation will fail if generation is set and it's not equal to
     * the generation on the server.
     *
     * @param generation    expected generation
     */
    public void set_generation(int generation) {
      mUseGeneration = true;
      mGeneration = generation;
    }

    /**
     * Specify highest expected generation.
     * <p>
     * Write operation will fail if generation is set and it's less than the
     * generation on the server. (Useful for restore after backup.)
     *
     * @param generation    highest expected generation
     */
    public void set_generation_gt(int generation) {
      mUseGenerationGt = true;
      mGeneration = generation;
    }

    /**
     * Specify expected generation, and flag to duplicate if generation is
     * not equal to that on server.
     * <p>
     * Write operation will generate duplicate record if expected generation
     * is set and it's not the generation on the server.
     *
     * @param generation    expected generation
     */
    public void set_generation_dup(int generation) {
      mUseGenerationDup = true;
      mGeneration = generation;
    }
  }

  /**
   * Container object for optional parameters used in scan operations.
   * <p>
   * This object is passed in <code>scan...()</code> calls to specify options.
   * <code>null</code> may be passed to use defaults for all options.
   */
  public static class ClScanningOptions {
    // Honored by client - work on nodes in parallel or serially:
    private boolean mConcurrentNodes;
    // Honored by client - have multiple threads per node:
    private int mThreadsPerNode;
    // Honored by server - priority of scan:
    private ClScanningPriority mPriority;
    // Honored by server - terminate scan if cluster in fluctuating state:
    private boolean mFailOnClusterChange;

    /**
     * Constructor, sets default scan options.
     * <p>
     * The object is constructed with concurrent nodes scan enabled, one
     * thread per node, scan priority {@link ClScanningPriority#AUTO}, and
     * scan termination on cluster changes disabled.
     */
    public ClScanningOptions() {
      mConcurrentNodes = true;
      mThreadsPerNode = 1;
      mPriority = ClScanningPriority.AUTO;
      mFailOnClusterChange = false;
    }

    /**
     * Get concurrent nodes scan setting.
     *
     * @return          <code>true</code> if concurrent nodes scan
     *               is enabled, <code>false</code> if not
     */
    public boolean isConcurrentNodes() {
      return mConcurrentNodes;
    }

    /**
     * Specify concurrent nodes scan setting.
     *
     * @param concurrentNodes  <code>true</code> to enable concurrent nodes
     *               scan, <code>false</code> to disable
     */
    public void setConcurrentNodes(boolean concurrentNodes) {
      mConcurrentNodes = concurrentNodes;
    }

    /**
     * Get number of client scan threads per node.
     *
     * @return          number of client threads used to scan a node
     */
    public int getThreadsPerNode() {
      return mThreadsPerNode;
    }

    /**
     * Specify number of client scan threads per node - not yet supported.
     *
     * @param threadsPerNode  number of client threads used to scan a node
     */
    public void setThreadsPerNode(int threadsPerNode) {
      mThreadsPerNode = threadsPerNode;
    }

    /**
     * Get server scan priority setting.
     *
     * @return          {@link ClScanningPriority scan priority} to
     *               be used by server
     */
    public ClScanningPriority getPriority() {
      return mPriority;
    }

    /**
     * Specify server scan priority setting.
     *
     * @param priority      {@link ClScanningPriority scan priority} to
     *               be used by server
     */
    public void setPriority(ClScanningPriority priority) {
      mPriority = priority;
    }

    /**
     * Get scan termination setting.
     *
     * @return          <code>true</code> if scan will terminate on
     *               cluster change, <code>false</code> if not
     */
    public boolean isFailOnClusterChange() {
      return mFailOnClusterChange;
    }

    /**
     * Specify scan termination setting.
     *
     * @param failOnClusterChange  <code>true</code> to terminate scan on
     *               cluster change, <code>false</code> to
     *               continue scan
     */
    public void setFailOnClusterChange(boolean failOnClusterChange) {
      mFailOnClusterChange = failOnClusterChange;
    }
  }

  //-------------------------------------------------------
  // Results of Operations
  //-------------------------------------------------------

  /**
   * Container object for database operation results.
   */
  public static class ClResult {
    /**
     * {@link ClResultCode Result code} for operation.
     */
    public ClResultCode resultCode = ClResultCode.NOT_SET;

    /**
     * How many times the record has been modified (including creation) on
     * the server.
     */
    public int generation = -1;

    /**
     * For scans, to notify that the data is corrupted.
     */
    public boolean corruptedData = false;

    /**
     * When only one bin is requested, value is returned here.
     */
    public Object result = null;

    /**
     * Requested bins are returned here, as name/value pairs.
     */
    public Map<String, Object> results = null;

    /**
     * When conflicting versions are encountered, every version's bins are
     * returned as an element of this <code>List</code>.
     * <p>
     * @see          ClWriteOptions#mUseGenerationDup
     */
    public List<Map<String, Object>> results_dup = null;

    // Package-Private
    // Used internally by batch operations to store per-node results.
    ClResult[] resultArray = null;

    /**
     * Constructor, creates "empty" object.
     */
    public ClResult() {
    }

    /**
     * Constructor, specifies result code.
     *
     * @param resultCode    operation {@link ClResultCode result code}
     */
    public ClResult(ClResultCode resultCode) {
      this.resultCode = resultCode;
    }

    /**
     * Convert result code to meaningful <code>String</code>.
     *
     * @param resultCode    operation {@link ClResultCode result code}
     * @return          interpretation of <code>resultCode</code>
     */
    public static String resultCodeToString(ClResultCode resultCode) {
      switch (resultCode) {
      case OK:
        return "OK";
      case NOT_SET:
        return "result code has not been set";
      case SERVER_ERROR:
        return "server error";
      case TIMEOUT:
        return "timeout";
      case CLIENT_ERROR:
        return "client error";
      case KEY_NOT_FOUND_ERROR:
        return "key not found error";
      case GENERATION_ERROR:
        return "generation error";
      case PARAMETER_ERROR:
        return "parameter error";
      case KEY_EXISTS_ERROR:
        return "key exists error";
      case BIN_EXISTS_ERROR:
        return "bin exists error";
      case SERIALIZE_ERROR:
        return "serialize error";
      case CLUSTER_KEY_MISMATCH:
        return "cluster key mismatch";
      case SERVER_MEM_ERROR:
        return "server memory error";
      case NO_XDS:
        return "XDS product not available";
      case SERVER_NOT_AVAILABLE:
        return "server not available";
      case BIN_TYPE_ERROR:
        return "bin type error";
      case RECORD_TOO_BIG:
        return "record too big";
      case KEY_BUSY:
        return "key busy";
      default:
        return "unknown error " + resultCode;
      }
    }
  }

  //-------------------------------------------------------
  // Container for Bin Name & Value
  //-------------------------------------------------------

  /**
   * Container object for record bin name/value pair.
   */
  public static class ClBin {
    /**
     * Bin name. Current limit is 14 characters.
     */
    public String name;

    /**
     * Bin value.
     */
    public Object value;

    /**
     * Constructor, sets <code>null</code> bin name and value.
     */
    public ClBin() {
      name = null;
      value = null;
    }

    /**
     * Constructor, specifying bin name and value.
     *
     * @param name        bin name, current limit is 14 characters
     * @param value        bin value
     */
    public ClBin(String name, Object value) {
      this.name = name;
      this.value = value;
    }
  }

  //-------------------------------------------------------
  // RTA Options and Filters
  //-------------------------------------------------------

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public static class ClAppendSegmentWriteOptions {
    // Segment expires this many seconds from now, 0 means use default.
    public int expiration;
    // User-defined, null means no tag.
    public String tag;
    // null means creation time is when server receives segment.
    public Date creation_time;

    public ClAppendSegmentWriteOptions(int expiration, String tag,
        Date creation_time) {
      this.expiration = expiration;
      this.tag = tag;
      this.creation_time = creation_time;
    }
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public static class ClAppendSegmentQueryFilter {
    // For range queries - return only segments created at or after
    // start_time, null means not used.
    public Date start_time;
    // For range queries - return only segments created before end_time,
    // null means not used.
    public Date end_time;
    // User-defined, matches if tag parameter is contained in segment tag,
    // null means not used.
    public String tag;

    public ClAppendSegmentQueryFilter(String tag, Date start_time,
        Date end_time) {
      this.tag = tag;
      this.start_time = start_time;
      this.end_time = end_time;
    }
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public static class ClBinAppendSegmentWriteOptions extends ClBin {
    public ClAppendSegmentWriteOptions write_opts;

    public ClBinAppendSegmentWriteOptions(String name, Object value,
        ClAppendSegmentWriteOptions write_opts) {
      this.name = name;
      this.value = value;
      this.write_opts = write_opts;
    }
  }

  /**
   * For use with special RTA servers only - not generally supported.
   */
  public static class ClBinAppendSegmentQueryOptions extends ClBin {
    public Collection <ClAppendSegmentQueryFilter> query_filters;
    public int max_results;

    public ClBinAppendSegmentQueryOptions(String name, Object value,
        Collection<ClAppendSegmentQueryFilter> query_filters,
        int max_results) {
      this.name = name;
      this.value = value;
      this.query_filters = query_filters;
      this.max_results = max_results;
    }
  }


  //=================================================================
  // Package-Private Functions
  //

  //-------------------------------------------------------
  // Logging Helpers
  //-------------------------------------------------------

  static void error(String msg) {
    ClLog(ClLogLevel.ERROR, msg);
  }

  static void warn(String msg) {
    ClLog(ClLogLevel.WARN, msg);
  }

  static void info(String msg) {
    ClLog(ClLogLevel.INFO, msg);
  }

  static void debug(String msg) {
    ClLog(ClLogLevel.DEBUG, msg);
  }

  static void verbose(String msg) {
    ClLog(ClLogLevel.VERBOSE, msg);
  }


  //=================================================================
  // Package-Private Nested Interfaces & Classes
  //

  //-------------------------------------------------------
  // Exceptions
  //-------------------------------------------------------

  static class SerializeException extends Exception {
    // Generated by random.org:
    static final long serialVersionUID = 0x7DF875330CD56169L;
  }


  //=================================================================
  // Private Functions
  //

  //-------------------------------------------------------
  // Argument Conversion Helpers
  //-------------------------------------------------------

  private static ArrayList<ClBin> binAsCollection(String name, Object value) {
    ArrayList<ClBin> bins = new ArrayList<ClBin>(1);

    bins.add(new ClBin(name, value));

    return bins;
  }

  private static HashMap<String, Object> binAsMap(String name, Object value) {
    HashMap<String, Object> bins = new HashMap<String, Object>();

    bins.put(name, value);

    return bins;
  }

  private static ArrayList<String> binNameAsCollection(String name) {
    ArrayList<String> binNames = new ArrayList<String>(1);

    binNames.add(name);

    return binNames;
  }

  //-------------------------------------------------------
  // Argument Checking Helpers
  //-------------------------------------------------------

  private static boolean isValidArgs(String tag, Object arg1, Object arg2,
      Object arg3, Object arg4, Object arg5) {
    if (! isValidArg(arg1)) {
      warn(tag + "(): unexpected null argument [1]");
      return false;
    }

    if (! isValidArg(arg2)) {
      warn(tag + "(): unexpected null argument [2]");
      return false;
    }

    if (! isValidArg(arg3)) {
      warn(tag + "(): unexpected null argument [3]");
      return false;
    }

    if (! isValidArg(arg4)) {
      warn(tag + "(): unexpected null argument [4]");
      return false;
    }

    if (! isValidArg(arg5)) {
      warn(tag + "(): unexpected null argument [5]");
      return false;
    }

    return true;
  }

  private static boolean isValidArgs(String tag, Object arg1, Object arg2,
      Object arg3, Object arg4) {
    if (! isValidArg(arg1)) {
      warn(tag + "(): unexpected null argument [1]");
      return false;
    }

    if (! isValidArg(arg2)) {
      warn(tag + "(): unexpected null argument [2]");
      return false;
    }

    if (! isValidArg(arg3)) {
      warn(tag + "(): unexpected null argument [3]");
      return false;
    }

    if (! isValidArg(arg4)) {
      warn(tag + "(): unexpected null argument [4]");
      return false;
    }

    return true;
  }

  private static boolean isValidArgs(String tag, Object arg1, Object arg2,
      Object arg3) {
    if (! isValidArg(arg1)) {
      warn(tag + "(): unexpected null argument [1]");
      return false;
    }

    if (! isValidArg(arg2)) {
      warn(tag + "(): unexpected null argument [2]");
      return false;
    }

    if (! isValidArg(arg3)) {
      warn(tag + "(): unexpected null argument [3]");
      return false;
    }

    return true;
  }

  private static boolean isValidArgs(String tag, Object arg1, Object arg2) {
    if (! isValidArg(arg1)) {
      warn(tag + "(): unexpected null argument [1]");
      return false;
    }

    if (! isValidArg(arg2)) {
      warn(tag + "(): unexpected null argument [2]");
      return false;
    }

    return true;
  }

  private static boolean isValidArg(Object arg) {
    if (arg == null) {
      return false;
    }

    if (arg instanceof ClBin) {
      if (! (isValidArg(((ClBin)arg).name) &&
          isValidArg(((ClBin)arg).value))) {
        return false;
      }
    }
    else if (arg instanceof Collection) {
      for (Object obj : (Collection<?>)arg) {
        if (! isValidArg(obj)) {
          return false;
        }
      }
    }
    else if (arg instanceof Map) {
      Map<?, ?> map = (Map<?, ?>)arg;

      for (Object key : map.keySet()) {
        if (! (isValidArg(key) && isValidArg(map.get(key)))) {
          return false;
        }
      }
    }

    return true;
  }

  //-------------------------------------------------------
  // Append & Prepend Operation Helpers
  //-------------------------------------------------------

  private ClResultCode pend(PendType pt, Object key, Object value) {
    return pend(pt, key, value, null, null);
  }

  private ClResultCode pend(PendType pt, Object key, Object value,
      ClOptions opts, ClWriteOptions wOpts) {
    return pend(pt, mDefaultNamespace, "", key, binAsCollection("", value),
        opts, wOpts);
  }

  private ClResultCode pend(PendType pt, String namespace, String set,
      Object key, String binName, Object value, ClOptions opts,
      ClWriteOptions wOpts) {
    return pend(pt, namespace, set, key, binAsCollection(binName, value),
        opts, wOpts);
  }

  private ClResultCode pend(PendType pt, String namespace, String set,
      Object key, Collection<ClBin> bins, ClOptions opts,
      ClWriteOptions wOpts) {
    if (! isValidArgs((pt == PendType.APPEND ? "ap" : "pre") + "pend",
        namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);

    switch (pt) {
    case PREPEND:
      asb.addPrepend(bins);
      break;
    case APPEND:
      asb.addAppend(bins);
      break;
    }

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  private ClResultCode pend(PendType pt, String namespace, String set,
      Object key, Map<String, Object> bins, ClOptions opts,
      ClWriteOptions wOpts) {
    if (! isValidArgs((pt == PendType.APPEND ? "ap" : "pre") + "pend",
        namespace, set, key, bins)) {
      return ClResultCode.PARAMETER_ERROR;
    }

    CLBuffer asb = CLBuffer.create();

    asb.set_required(namespace, set, key, wOpts);

    switch (pt) {
    case PREPEND:
      asb.addPrepend(bins);
      break;
    case APPEND:
      asb.addAppend(bins);
      break;
    }

    ClResult r = doRequest(asb, opts);

    asb.destroy();

    return r.resultCode;
  }

  //-------------------------------------------------------
  // Batch Get Operation Helper
  //-------------------------------------------------------

  // The getAll argument will override the bins argument - if getAll is true,
  // all the bins will be fetched.
  //
  // If successful, this function will return an array of ClResult objects
  // whose size is equal to the number of keys passed in. The ClResult array
  // is positionally matched with the keys. If there is no record for a
  // requested key in the database, its corresponding result will be null. In
  // some error cases, this function will return a single-element array with a
  // suggestive error code.
  private ClResult[] batchGetGeneric(String namespace, String set,
      Collection<Object> keys, Collection<String> bins, ClOptions opts,
      boolean getAll, boolean getData) {
    if (! isValidArgs("batchGet", namespace, set, keys)) {
      ClResult[] results = new ClResult[1];
      results[0] = new ClResult(ClResultCode.PARAMETER_ERROR);
      return results;
    }

    int numKeys = keys.size();

    if (numKeys == 0) {
      return null;
    }

    if (mConnMgr == null) {
      ClResult[] results = new ClResult[1];
      results[0] = new ClResult(ClResultCode.CLIENT_ERROR);
      return results;
    }

    ClResult[] results = new ClResult[numKeys];

    // Hack - 255 node-IDs should be enough. (Note: node-ID is not same as
    // number of nodes.) We are using an indexable array because we need
    // quick access to the node when adding keys to its batch payload.
    int numNodes = 255;

    // I don't know how to declare this without the warning. [AKG]
    @SuppressWarnings("unchecked")
    ArrayList<BatchElement>[] batchLists = new ArrayList[numNodes];
    String[] nodeNames = new String[numNodes];

    for (int n = 0; n < numNodes; n++) {
      batchLists[n] = new ArrayList<BatchElement>();
    }

    IMessageDigest md = HashFactory.getInstance("RIPEMD-160");
    CLBuffer clBuffer = CLBuffer.create();

    byte[] buf = new byte[512]// for holding set and key

    int setOffset = 0;
    int setLen = CLBuffer.stringToUtf8(set, buf, setOffset);
    int keyOffset = setLen + 1;

    // Create the digests for all the keys passed in.
    Iterator<Object> keysIterator = keys.iterator();

    for (int i = 0; keysIterator.hasNext(); i++) {
      Object key = keysIterator.next();
      int keyLen = clBuffer.keyToBytes(key, buf, keyOffset);

      md.reset();
      md.update(buf, setOffset, setLen);
      md.update(buf, keyOffset, keyLen);

      byte[] digest = md.digest();
      long partitionId = CLBuffer.get_ntohl_intel(digest, 0);

      // Can't use mod directly - mod will give negative numbers. First
      // bitwise-and determines positive/negative correctly, then mod.
      partitionId = (partitionId & 0xFFFF) %
          CLConnectionManager.n_server_partitions;

      CLNode node = mConnMgr.partitionWriteHashMap.get(
          String.format("%s:%d", namespace, partitionId));

      if (node == null) {
        batchLists[0].add(new BatchElement(i, key, digest));
        nodeNames[0] = null;
      }
      else {
        batchLists[node.id].add(new BatchElement(i, key, digest));
        nodeNames[node.id] = node.name;
      }
    }

    clBuffer.destroy();

    // Dispatch the work to each node on a different thread.
    ArrayList<Thread> allThreads = new ArrayList<Thread>(batchLists.length);

    for (int i = 0; i < batchLists.length; i++) {
      if (batchLists[i].size() > 0) {
        NodeBatchExecutor nbe = new NodeBatchExecutor(results,
            batchLists[i], namespace, set, bins, opts, getAll,
            getData, nodeNames[i]);

        Thread t = new Thread(nbe, "node" + i);

        allThreads.add(t);
        t.start();
      }
    }

    // Wait for all the threads to finish their work and return results.
    Iterator<Thread> threadsIterator = allThreads.iterator();

    while (threadsIterator.hasNext()) {
      Thread t = threadsIterator.next();

      try {
        t.join();
      }
      catch (Exception e) {
        info("thread " + t.getId() + " faced some issue: " + e);
      }
    }

    return results;
  }

  //-------------------------------------------------------
  // Transaction Helpers
  //-------------------------------------------------------

  private ClResult doRequestUsingNode(CLBuffer asb, CLNode node,
      ClOptions opts) {
    if (mConnMgr == null) {
      return new ClResult(ClResultCode.CLIENT_ERROR);
    }

    int timeout = opts != null ? opts.mTimeout : DEFAULT_TIMEOUT;
    int sleepMillis = timeout / RETRY_COUNT;

    if (sleepMillis < MIN_TIMEOUT_MS_WAIT) {
      sleepMillis = MIN_TIMEOUT_MS_WAIT;
    }

    if (sleepMillis > MAX_TIMEOUT_MS_WAIT) {
      sleepMillis = MAX_TIMEOUT_MS_WAIT;     
    }   

    int remainingMillis = timeout;
    int tryCount = 0;
    ClResult r = null;
    long stop = System.currentTimeMillis() + timeout;

    do {
      CLConnection conn = null;

      try {
        conn = mConnMgr.getConnection(asb.ns, node, remainingMillis);

        int newRemainingMillis =
            (int)(stop - System.currentTimeMillis());

        if (conn != null && newRemainingMillis <= 0) {
          info("connected to " + conn.address.getHostName() +
              " within " + remainingMillis +
              "ms but timed out on try " + tryCount +
              " (timeout = " + timeout + ")");

          mConnMgr.releaseConnection(conn, true);
          break;
        }

        remainingMillis = newRemainingMillis;

        if (conn != null) {
          asb.setTimeout(remainingMillis);
          conn.setTimeout(remainingMillis);
          conn.send(asb);
          r = conn.retrieve(asb);
          conn.reportNetworkSuccess();
        }
        else {
          info("could not get connection");
        }
      }
      // Serialize errors aren't node's fault - don't dunn, don't retry.
      catch (SerializeException se) {
        info("doRequestUsingNode(): serialize exception");

        // conn won't be null here.
        mConnMgr.releaseConnection(conn, true);
        return new ClResult(ClResultCode.SERIALIZE_ERROR);
      }
      // IO exceptions are the fault of the node - decrease node health.
      catch (IOException ioe) {
        info("doRequestUsingNode(): IO exception '" + ioe.toString() +
            "' when connecting to " +
            conn.address.getHostName() + " with " +
            remainingMillis + "ms remaining");

        // conn won't be null here.
        conn.reportNetworkFailure();
        mConnMgr.releaseConnection(conn, false);
        conn = null;
      }
      catch (Exception e) {
        info("doRequestUsingNode(): exception '" + e.toString() + "'");
        //e.printStackTrace();
      }

      // Always return to pool if no error.
      if (conn != null) {
        mConnMgr.releaseConnection(conn, true);
        conn = null;
      }

      if (r == null && ClOptions.wantsRetry(opts)) {
        // Break if we exceeded caller-specified timeout.
        remainingMillis = (int)(stop - System.currentTimeMillis());

        if (remainingMillis <= sleepMillis) {
          break;
        }

        debug("doRequestUsingNode(): sleeping " + sleepMillis + "ms");

        try {
          Thread.sleep((long)sleepMillis);
        }
        catch (InterruptedException e) {
          break;
        }

        tryCount++;
      }
    } while (r == null && ClOptions.wantsRetry(opts));

    if (r == null) {
      r = new ClResult(ClResultCode.TIMEOUT);
    }

    return r;
  }

  // Prepare the buffer, then do the transaction.
  private ClResult doRequest(CLBuffer asb, ClOptions opts) {
    try {
      asb.prepare();
    }
    catch (SerializeException e) {
      return new ClResult(ClResultCode.SERIALIZE_ERROR);
    }

    return doRequestPrepared(asb, opts);
  }

  // Once we have a buffer, loop getting connections to nodes and making the
  // transactions happen.
  private ClResult doRequestPrepared(CLBuffer asb, ClOptions opts) {
    if (mConnMgr == null) {
      return new ClResult(ClResultCode.CLIENT_ERROR);
    }

    int timeout = opts != null ? opts.mTimeout : DEFAULT_TIMEOUT;
    long waitNanos = ((long)timeout * 1000000L) / 6;

    // Less than a half a millisecond will usually hurt a server.
    if (waitNanos < 500000) {
      waitNanos = 500000;
    }

    int tries = 0;
    ClResult r;
    long start = System.currentTimeMillis();

    do {
      if (tries > 50) {
        info("transaction with LARGE (" + tries + ") retries, " +
            (System.currentTimeMillis() - start) + "ms");
      }
      else if (tries > 3) {
        info("transaction with " + tries + " retries, " +
            (System.currentTimeMillis() - start) + "ms");
      }

      tries++;

      r = null;
      CLConnection conn = null;

      try {
        conn = mConnMgr.getConnection(asb.ns, (int)asb.partition_id,
            asb.partition_type, timeout);

        int deltaMillis = (int)(System.currentTimeMillis() - start)

        if (deltaMillis < 0) {
          deltaMillis = 0;
        }

        if (conn != null) {
          if (deltaMillis >= timeout) {
            info("connected to " + conn.address.getHostName() +
                " in " + deltaMillis +
                "ms but timed out (timeout = " + timeout + ")");

            mConnMgr.releaseConnection(conn, true);
            break;
          }

          asb.setTimeout(timeout - deltaMillis);
          conn.setTimeout(timeout - deltaMillis);
          conn.send(asb);
          r = conn.retrieve(asb);
          conn.reportNetworkSuccess();
        }
        else {
          info("could not get connection");
        }
      }
      // Serialize errors aren't node's fault - don't dunn, don't retry.
      catch (SerializeException se) {
        info("doRequest(): serialize exception");

        // conn won't be null here.
        mConnMgr.releaseConnection(conn, true);
        return new ClResult(ClResultCode.SERIALIZE_ERROR);
      }
      // IO exceptions are the fault of the node - decrease node health.
      catch (IOException ioe) {
        int deltaMillis = (int)(System.currentTimeMillis() - start);

        if (conn != null) {
          info("doRequest(): IO exception '" + ioe.toString() +
              "' when connecting to " +
              conn.address.getHostName() + " after " +
              deltaMillis + "ms");
        }
        else {
          info("doRequest(): IO exception '" + ioe.toString() +
              "' after " + deltaMillis + "ms");
        }
       
        //ioe.printStackTrace();

        if (conn != null) {
          conn.reportNetworkFailure();
          mConnMgr.releaseConnection(conn, false);
          conn = null;
        }
      }
      catch (Exception e) {
        info("doRequest(): exception '" + e.toString() + "'");
        //e.printStackTrace();
      }

      // Always return to pool if no error.
      if (conn != null) {
        mConnMgr.releaseConnection(conn, true);
        conn = null;
      }

      // A strange pathological case has been observed where a thread
      // grabs connections, is able to send to the server, but gets
      // immediate "connection closed" read errors. For this reason, use a
      // form of exponential retry - something that at least gives you
      // some quick retry, but backs off if there are consistant errors of
      // this type. The error we're seeing now (Jan 2011) seems more like
      // a JVM oddity, but we must protect the service from wayward
      // clients. Thus, back off on any kind of exception. [BB]
      //
      // Also this is informative (perhaps dated by the time you read it):
      // http://www.sagui.org/~gustavo/blog/code

      if (r == null && ClOptions.wantsRetry(opts)) {
        // Break if we exceeded caller-specified timeout.
        int deltaMillis = (int)(System.currentTimeMillis() - start);

        if (deltaMillis < 0) {
          deltaMillis = 0;
        }

        if (deltaMillis + (int)(waitNanos / 1000000) >= timeout) {
          break;
        }

        info("doRequest() waiting " + waitNanos + "ns");

        long nanoEnd = System.nanoTime() + waitNanos;

        do {
          LockSupport.parkNanos(waitNanos);
        } while (System.nanoTime() < nanoEnd);

        waitNanos *= 2.2;
      }
    } while (r == null && ClOptions.wantsRetry(opts));

    if (r == null) {
      r = new ClResult(ClResultCode.TIMEOUT);
    }

    return r;
  }

  //-------------------------------------------------------
  // Logging Helpers
  //-------------------------------------------------------

  private static void ClLogInit() {
    if (gSdf == null) {
      gSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
    }
  }

  public static void ClLog(ClLogLevel logLevel, String msg) {
    if (logLevel.ordinal() > gLogLevel.ordinal()) {
      return;
    }

    if (gLogCallback != null) {
      gLogCallback.logCallback(logLevel, msg);
      return;
    }

    String date = gSdf != null ? gSdf.format(new Date()) : "";

    System.err.println(date + " CITRUSLEAF [" +
        Thread.currentThread().getId() + "] " + msg);
  }


  //=================================================================
  // Private Nested Interfaces & Classes
  //

  private static class BatchElement {
    public int posInGlobalBatch;
    public Object key;
    public byte[] digest;

    public BatchElement(int index, Object key, byte[] digest) {
      posInGlobalBatch = index;
      this.key = key;
      this.digest = digest;
    }
  }

  private class NodeBatchExecutor implements Runnable {
    private ClResult[] mAllResults;
    private ArrayList<BatchElement> mBatchList;
    private String mNamespace;
    private String mSet;
    private boolean mGetAll;
    private boolean mGetData;
    private ClOptions mOpts;
    private Collection<String> mBins;
    private String mNodeName;

    public NodeBatchExecutor(ClResult[] allResults,
        ArrayList<BatchElement> batchList, String namespace, String set,
        Collection<String> bins, ClOptions opts, boolean getAll,
        boolean getData, String nodeName) {
      mAllResults = allResults;
      mBatchList = batchList;
      mNamespace = namespace;
      mSet = set;
      mBins = bins;
      mGetAll = getAll;
      mGetData = getData;
      mOpts = opts;
      mNodeName = nodeName;
    }

    public void run() {
      CLBuffer asb = CLBuffer.create();

      ArrayList<Object> nodeKeys = new ArrayList<Object>();
      ArrayList<byte[]> nodeDigests = new ArrayList<byte[]>();
      Iterator<BatchElement> listIterator = mBatchList.iterator();

      while (listIterator.hasNext()) {
        BatchElement elem = listIterator.next();

        nodeKeys.add(elem.key);
        nodeDigests.add(elem.digest);
      }

      // Request a batch operation.
      asb.set_required(mNamespace, mSet, null, null);
      asb.setBatchRead(nodeKeys, nodeDigests);

      if (mGetAll) {
        asb.readAll();
      }
      else if (mGetData) {
        asb.addRead(mBins);
      }
      else {
        asb.readExists();
        asb.readNoBindata();
      }

      // Prepare the command message to be sent to the server node.
      try {
        asb.batchPrepare();
      }
      catch (SerializeException e) {
        asb.destroy();
        fillError(ClResultCode.SERIALIZE_ERROR);
        return;
      }

      // Send the message to the server node and get back the result.
      CLNode node = mNodeName != null ?
          mConnMgr.getNodeFromNodeName(mNodeName) : null;
      ClResult r = node != null ?
          doRequestUsingNode(asb, node, mOpts) :
          doRequestPrepared(asb, mOpts);

      asb.destroy();

      if (r.resultArray == null)
      {
        fillError(ClResultCode.CLIENT_ERROR);
        return;
      }

      if (r.resultCode != ClResultCode.OK) {
        fillError(r.resultCode);
        return;
      }

      Iterator<BatchElement> resultIterator = mBatchList.iterator();

      for (int i = 0; resultIterator.hasNext(); i++) {
        BatchElement elem = resultIterator.next();

        mAllResults[elem.posInGlobalBatch] = r.resultArray[i];
      }
    }

    private void fillError(ClResultCode code) {
      Iterator<BatchElement> resultIterator = mBatchList.iterator();

      while (resultIterator.hasNext()) {
        BatchElement elem = resultIterator.next();

        mAllResults[elem.posInGlobalBatch] = new ClResult(code);
      }
    }
  }
}
TOP

Related Classes of net.citrusleaf.CitrusleafClient

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.