# -*- cperl -*-
# Copyright (c) 2011, 2021, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is also distributed with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms,
# as designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have included with MySQL.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

########## Memcache Client Library for Perl
###
###  $mc = My::Memcache->new()          create an ascii-protocol client
###  $mc = My::Memcache::Binary->new()  create a binary-protocol client
###
###  $mc->connect(host, port)           returns 1 on success, 0 on failure
###
###  $mc->{error}                       holds most recent error/status message
###
###  $mc->store(cmd, key, value, ...)   alternate API for set/add/replace/append/prepend
###  $mc->set(key, value)               returns 1 on success, 0 on failure
###  $mc->add(key, value)               set if record does not exist
###  $mc->replace(key, value)           set if record exists
###  $mc->append(key, value)            append value to existing data
###  $mc->prepend(key, value)           prepend value to existing data
###
###  $mc->get(key, [ key ...])          returns a value or undef
###  $mc->next_result()                 Fetch results after get()
###
###  $mc->delete(key)                   returns 1 on success, 0 on failure
###  $mc->stats(stat_key)               get stats; returns a hash
###  $mc->incr(key, amount, [initial])  returns the new value or undef
###  $mc->decr(key, amount, [initial])  like incr.
###                                           The third argument is used in
###                                           the binary protocol ONLY.
###  $mc->flush()                       flush_all
###
###  $mc->set_expires(sec)              Set TTL for all store operations
###  $mc->set_flags(int_flags)          Set numeric flags for store operations
###
###  $mc->note_config_version()
###    Store the generation number of the running config in the filesystem,
###    for later use by wait_for_reconf()
###
###  $mc->wait_for_reconf()
###    Wait for NDB/Memcache to complete online reconfiguration.
###    Returns the generation number of the newly running configuration,
###    or zero on timeout/error.

################ TODO ################################
###  * Support explicit binary k/q commands with pipelining
###  * Implement TOUCH & GAT commands
###  * Support UDP
###  * Standardize APIs to take (key, value, hashref-of-options)

use strict;
use IO::Socket::INET;
use IO::File;
use Carp;
use Time::HiRes;
use Errno qw( EWOULDBLOCK );

######## Memcache Result

package My::Memcache::Result;

sub new {
  my ($pkg, $key, $flags, $cas) = @_;
  $cas = 0 if (!defined($cas));
  bless { "key"   => $key,
          "flags" => $flags,
          "cas"   => $cas,
          "value" => undef,
  }, $pkg;
}

######## Memcache Client

package My::Memcache;

sub new {
  my $pkg = shift;
  # min/max wait refer to msec. wait during temporary errors.  Both powers of 2.
  # io_timeout is in seconds (possibly fractional)
  # io_timeout * max_read_tries = read timeout
  bless { "created"         => 1,
          "error"           => "OK",
          "cf_gen"          => 0,
          "req_id"          => 0,
          "min_wait"        => 4,
          "max_wait"        => 8192,
          "temp_errors"     => 0,
          "total_wait"      => 0,
          "has_cas"         => 0,
          "flags"           => 0,
          "exptime"         => 0,
          "get_results"     => undef,
          "get_with_cas"    => 0,
          "failed"          => 0,
          "io_timeout"      => 5.0,
          "sysread_size"    => 512,
          "max_read_tries"  => 6,
          "readbuf"         => "",
          "buflen"          => 0,
          "error_detail"    => "",
          "read_try"        => 0,
          "max_write_tries" => 6
  }, $pkg;
}

sub summarize {
  my $val = shift;
  my $len = length $val;

  if ($len > 25) {
    return substr($val, 0, 10) . "..." . substr($val, -10) . " [len $len]";
  } else {
    return $val;
  }
}

# fail() is called when an MTR test fails
sub fail {
  my $self = shift;
  my $fd;

  if ($self->{failed}) {
    print STDERR " /// My::Memcache::fail() called recursively.\n";
    return;
  }
  $self->{failed} = 1;

  my $msg =
    "error: " .
    $self->{error} . "\t" . "read_try: " . $self->{read_try} . "\t" .
    "protocol: " . $self->protocol() . "\n" . "req_id: " . $self->{req_id} .
    "\t" . "temp err wait: " . $self->{total_wait} . " msec.\n";

  $msg .= "detail: " . $self->{error_detail} . "\n";
  $msg .= "buffer: " . summarize($self->{readbuf}) . "\n";

  my $r = $self->next_result();
  $msg .= "value: " . summarize($r->{value}) . "\n" if ($r);

  while (my $extra = shift) {
    $msg .= $extra;
  }
  $msg .= "\n";

  $msg .= $self->get_server_error_stats();

  # Load Average on linux
  $msg .= ("Load Avg: " . <$fd>) if (open($fd, "/proc/loadavg"));

  $msg .= "====~~~~____~~~~====\n";

  Carp::confess($msg);
}

# Attempt a new connection to memcached to flush the server's error log
# and obtain error statistics

sub get_server_error_stats {
  my $self       = shift;
  my $new_client = My::Memcache::Binary->new();
  my $r          = $new_client->connect($self->{host}, $self->{port});
  my $msg        = "";

  if ($r) {
    my %stats = $new_client->stats("errors");    # Also flushes server log
    $msg .= "Server error stats:\n";
    $msg .= sprintf("%s : %s\n", $_, $stats{$_}) for keys(%stats);
  } else {
    $msg =
      "Attempted new server connection to fetch error statistics but failed.\n";
  }
  return $msg;
}

# Common code to ASCII and BINARY protocols:

sub connect {
  my $self = shift;
  my $host = shift;
  my $port = shift;
  my $conn;

  # Wait for memcached to be ready, up to ten seconds.
  my $retries = 100;
  do {
    $conn = IO::Socket::INET->new(PeerAddr => "$host:$port", Proto => "tcp");
    if (!$conn) {
      Time::HiRes::usleep(100 * 1000);
      $retries--;
    }
  } while ($retries && !$conn);

  if ($conn) {
    $conn->blocking(0);    # Set non-blocking
    my $fd    = fileno $conn;
    my $fdset = '';
    vec($fdset, $fd, 1) = 1;
    $self->{fdset}      = $fdset;
    $self->{connection} = $conn;
    $self->{host}       = $host;
    $self->{port}       = $port;
    return 1;
  }
  $self->{error} = "CONNECTION_FAILED";
  return 0;
}

sub DESTROY {
  my $self = shift;
  if ($self->{connection}) {
    $self->{connection}->close();
  }
}

sub set_expires {
  my $self = shift;
  $self->{exptime} = shift;
}

sub set_flags {
  my $self = shift;
  $self->{flags} = shift;
}

# Some member variables are per-request.
# Clear them in preparation for a new request, and increment the request counter.
sub new_request {
  my $self = shift;
  $self->{error}    = "OK";
  $self->{read_try} = 0;
  $self->{has_cas}  = 0;
  $self->{req_id}++;
  $self->{get_results} = undef;
}

sub next_result {
  my $self = shift;
  shift @{ $self->{get_results} };
}

# note_config_version and wait_for_reconf are only for use by mysql-test-run
sub note_config_version {
  my $self = shift;

  my $vardir = $ENV{MYSQLTEST_VARDIR};
  # Fetch the memcached current config generation number and save it
  my %stats = $self->stats("reconf");
  my $F     = IO::File->new("$vardir/tmp/memcache_cf_gen", "w") or die;
  my $ver   = $stats{"Running"};
  print $F "$ver\n";
  $F->close();

  $self->{cf_gen} = $ver;
}

sub wait_for_reconf {
  my $self = shift;

  if ($self->{cf_gen} == 0) {
    my $cfgen  = 0;
    my $vardir = $ENV{MYSQLTEST_VARDIR};
    my $F      = IO::File->new("$vardir/tmp/memcache_cf_gen", "r");
    if (defined $F) {
      chomp($cfgen = <$F>);
      undef $F;
      unlink("$vardir/tmp/memcache_cf_gen");
    }
    $self->{cf_gen} = $cfgen;
  }

  my $wait_for = $self->{cf_gen} + 1;

  my $new_gen = $self->wait_for_config_generation($wait_for);
  if ($new_gen > 0) {
    $self->{cf_gen} = $new_gen;
  } else {
    print STDERR "Wait for config generation $wait_for timed out.\n";
  }

  return $new_gen;
}

# wait_for_config_generation($cf_gen)
# Wait until memcached is running config generation >= to $cf_gen
# Returns 0 on error/timeout, or the actual running generation number
#
sub wait_for_config_generation {
  my $self    = shift;
  my $cf_gen  = shift;
  my $ready   = 0;
  my $retries = 100;     # 100 retries x 100 ms = 10s

  while ($retries && !$ready) {
    Time::HiRes::usleep(100 * 1000);
    my %stats = $self->stats("reconf");
    if ($stats{"Running"} >= $cf_gen) {
      $ready = $stats{"Running"};
    } else {
      $retries -= 1;
    }
  }
  return $ready;
}

#  -----------------------------------------------------------------------
#  --------------        Low-level Network Handling        ---------------
#  -----------------------------------------------------------------------

# Utility function sets error based on network error & returns false.
sub socket_error {
  my $self   = shift;
  my $retval = shift;
  my $detail = shift;

  if ($retval == 1) {
    $self->{error} = "CONNECTION_CLOSED";
  } elsif ($retval == 0) {
    $self->{error} = "NETWORK_TIMEOUT";
  } else {
    $self->{error} = "NETWORK_ERROR: " . $!;
  }
  $self->{error_detail} = $detail if ($detail);

  return 0;
}

# $mc->write(packet).  Returns true on success, false on error.
sub write {
  my $self    = shift;
  my $packet  = shift;
  my $len     = length($packet);
  my $nsent   = 0;
  my $attempt = 0;
  my $r;

  if (!$self->{connection}->connected()) {
    return $self->socket_error(0, "write(): not connected");
  }

  while ($nsent < $len) {
    $r = select(undef, $self->{fdset}, undef, $self->{io_timeout});
    if ($r < 1) {
      if (++$attempt >= $self->{max_write_tries}) {
        return $self->socket_error($r, "write(): select() returned $r");
      }
    } else {
      $r = $self->{connection}->send(substr($packet, $nsent));
      if ($r > 0) {
        $nsent += $r;
      } elsif ($! != Errno::EWOULDBLOCK) {
        return $self->socket_error($r, "write(): send() errno $!");
      }
    }
  }
  return 1;
}

# $mc->read(desired_size).  Low-level read.  Returns true on success,
# appends data to readbuf, and sets buflen.  Returns false on error.
sub read {
  my $self   = shift;
  my $length = shift;
  my $sock   = $self->{connection};
  my $r;

  if ($length > 0) {
    $r = select($self->{fdset}, undef, undef, $self->{io_timeout});
    return $self->socket_error($r, "read(): select() $!") if ($r < 0);

    $r = $sock->sysread($self->{readbuf}, $length, $self->{buflen});
    if ($r > 0) {
      $self->{buflen} += $r;
    } elsif ($r < 0 && $! != Errno::EWOULDBLOCK) {
      return $self->socket_error($r == 0 ? 1 : $r, "read(): sysread() $!");
    }
  }
  return 1;
}

# Utility routine; assumes $len is available on buffer.
sub chop_from_buffer {
  my $self = shift;
  my $len  = shift;

  my $line = substr($self->{readbuf}, 0, $len);
  $self->{readbuf} = substr($self->{readbuf}, $len);
  $self->{buflen} -= $len;
  return $line;
}

# Returns a line if available; otherwise undef
sub get_line_from_buffer {
  my $self = shift;
  my $line = undef;

  my $idx = index($self->{readbuf}, "\r\n");
  if ($idx >= 0) {
    $line = $self->chop_from_buffer($idx + 2);    # 2 for \r\n
  }
  return $line;
}

# Returns length if available; otherwise undef
sub get_length_from_buffer {
  my $self = shift;
  my $len  = shift;

  if ($self->{buflen} >= $len) {
    return $self->chop_from_buffer($len);
  }
  return undef;
}

# Read up to newline.  Returns a line, or sets and returns error.
sub read_line {
  my $self = shift;
  my $message;

  $self->{read_try} = 0;
  while ((!defined($message)) && $self->{read_try} < $self->{max_read_tries}) {
    $self->{read_try}++;
    $message = $self->get_line_from_buffer();
    if (!defined($message)) {
      if (!$self->read($self->{sysread_size})) {
        return $self->{error};
      }
    }
  }
  if (defined($message)) {
    $self->normalize_error($message);    # Handle server error responses
    return $message;
  }

  $self->socket_error(0, "read_line(): timeout");
  return $self->{error};
}

# Read <length> bytes.  Returns the data, or returns undef and sets error.
sub read_known_length {
  my $self = shift;
  my $len  = shift;
  my $data;
  my $pre_buflen;

  $self->{read_try} = 0;
  while ($self->{read_try} < $self->{max_read_tries}) {
    $data = $self->get_length_from_buffer($len);
    return $data if (defined($data));
    # Desired length is not yet in buffer.
    $pre_buflen = $self->{buflen};
    if (!$self->read($len - $pre_buflen)) {  # limited by io_timeout
      return undef;   # read error
    }
    Carp::croak if ($self->{buflen} < $pre_buflen);
    if($self->{buflen} == $pre_buflen) {
      $self->{read_try}++;  # select() timed out. Nothing was read.
    }
  }
  # Perhaps the read completed on the final attempt
  $data = $self->get_length_from_buffer($len);
  if (!defined($data)) {
    $self->socket_error(0, "read_known_length(): timeout");
  }
  return $data;
}

#  -----------------------------------------------------------------------
#  ------------------          ASCII PROTOCOL         --------------------
#  -----------------------------------------------------------------------

sub protocol {
  return "ascii";
}

sub protocol_error {
  my $self   = shift;
  my $detail = shift;

  if ($self->{error} eq "OK") {
    $self->{error} = "PROTOCOL_ERROR";
  }
  if ($detail) {
    $self->{error_detail} = $detail;
  }
  return undef;
}

sub ascii_command {
  my $self     = shift;
  my $packet   = shift;
  my $waitTime = $self->{min_wait};
  my $maxWait  = $self->{max_wait};
  my $reply;

  do {
    $self->new_request();
    $self->write($packet);
    $reply = $self->read_line();
    if ($self->{error} eq "SERVER_TEMPORARY_ERROR" && $waitTime < $maxWait) {
      $self->{temp_errors} += 1;
      $self->{total_wait}  += (Time::HiRes::usleep($waitTime * 1000) / 1000);
      $waitTime *= 2;
    }
    } while ($self->{error} eq "SERVER_TEMPORARY_ERROR" &&
             $waitTime <= $maxWait);

  return $reply;
}

sub delete {
  my $self     = shift;
  my $key      = shift;
  my $response = $self->ascii_command("delete $key\r\n");
  return 1 if ($response =~ "^DELETED");
  return 0 if ($response =~ "^NOT_FOUND");
  return 0 if ($response =~ "^SERVER_ERROR");
  return $self->protocol_error("delete() got response: $response");
}

sub store {
  my ($self, $cmd, $key, $value, $flags, $exptime, $cas_chk) = @_;
  $flags   = $self->{flags}   unless $flags;
  $exptime = $self->{exptime} unless $exptime;
  my $packet;
  if (($cmd eq "cas" || $cmd eq "replace") && $cas_chk > 0) {
    $packet = sprintf("cas %s %d %d %d %d\r\n%s\r\n",
                      $key, $flags, $exptime, $cas_chk, length($value), $value);
  } else {
    $packet = sprintf("%s %s %d %d %d\r\n%s\r\n",
                      $cmd, $key, $flags, $exptime, length($value), $value);
  }
  my $response = $self->ascii_command($packet);
  return 1 if ($response =~ "^STORED");
  return 0 if ($response =~ "^NOT_STORED");
  return 0 if ($response =~ "^EXISTS");
  return 0 if ($response =~ "^NOT_FOUND");
  return 0 if ($response =~ "^SERVER_ERROR");
  return $self->protocol_error("store() got response: $response");
}

sub set {
  my ($self, $key, $value, $flags, $exptime) = @_;
  return $self->store("set", $key, $value, $flags, $exptime);
}

sub add {
  my ($self, $key, $value, $flags, $exptime) = @_;
  return $self->store("add", $key, $value, $flags, $exptime);
}

sub append {
  my ($self, $key, $value, $flags, $exptime) = @_;
  return $self->store("append", $key, $value, $flags, $exptime);
}

sub prepend {
  my ($self, $key, $value, $flags, $exptime) = @_;
  return $self->store("prepend", $key, $value, $flags, $exptime);
}

sub replace {
  my ($self, $key, $value, $flags, $exptime, $cas) = @_;
  return $self->store("replace", $key, $value, $flags, $exptime, $cas);
}

sub get {
  my $self = shift;
  my @results;
  my $keys = "";
  $keys .= shift(@_) . " " while (@_);
  my $command = $self->{get_with_cas} ? "gets" : "get";
  $self->{get_with_cas} = 0;    # CHECK, THEN RESET FOR NEXT CALL
  my $response = $self->ascii_command("$command $keys\r\n");
  return undef if ($self->{error} ne "OK");

  while ($response ne "END\r\n") {
    $response =~ /^VALUE (\S+) (\d+) (\d+) ?(\d+)?/;
    if (!(defined($1) && defined($2) && defined($3))) {
      return $self->protocol_error("GET response: $response");
    }
    my $result = My::Memcache::Result->new($1, $2, $4);
    my $value = $self->read_known_length($3);
    return undef if (!defined($value));
    $result->{value} = $value;
    $self->read_line();    # Get trailing \r\n after value
    $self->{has_cas} = 1 if ($4);
    push @results, $result;
    $response = $self->read_line();
  }
  $self->{get_results} = \@results;
  return $results[0]->{value} if @results;
  $self->{error} = "NOT_FOUND";
  return undef;
}

sub _txt_math {
  my ($self, $cmd, $key, $delta) = @_;
  my $response = $self->ascii_command("$cmd $key $delta \r\n");

  if ($response =~ "^NOT_FOUND" || $response =~ "ERROR") {
    return undef;
  }

  $response =~ /(\d+)/;
  return $self->protocol_error("MATH response: $response") unless defined($1);
  return $1;
}

sub incr {
  my ($self, $key, $delta) = @_;
  return $self->_txt_math("incr", $key, $delta);
}

sub decr {
  my ($self, $key, $delta) = @_;
  return $self->_txt_math("decr", $key, $delta);
}

sub stats {
  my $self = shift;
  my $key = shift || "";

  $self->new_request();
  $self->write("stats $key\r\n");

  my %response = ();
  my $line     = $self->read_line();
  while ($line !~ "^END") {
    return %response if $line eq "ERROR\r\n";
    if (($line) && ($line =~ /^STAT(\s+)(\S+)(\s+)(\S+)/)) {
      $response{$2} = $4;
    } else {
      return $self->protocol_error("STATS response line: $line");
    }
    $line = $self->read_line();
  }

  return %response;
}

sub flush {
  my $self   = shift;
  my $key    = shift;
  my $result = $self->ascii_command("flush_all\r\n");
  return ($self->{error} eq "OK");
}

# Try to provide consistent error messagees across ascii & binary protocols
sub normalize_error {
  my $self  = shift;
  my $reply = shift;
  my %error_message = (
             "STORED\r\n"                                  => "OK",
             "EXISTS\r\n"                                  => "KEY_EXISTS",
             "NOT_FOUND\r\n"                               => "NOT_FOUND",
             "NOT_STORED\r\n"                              => "NOT_STORED",
             "CLIENT_ERROR value too big\r\n"              => "VALUE_TOO_LARGE",
             "SERVER_ERROR object too large for cache\r\n" => "VALUE_TOO_LARGE",
             "CLIENT_ERROR invalid arguments\r\n" => "INVALID_ARGUMENTS",
             "SERVER_ERROR not my vbucket\r\n"    => "NOT_MY_VBUCKET",
             "SERVER_ERROR out of memory\r\n"     => "SERVER_OUT_OF_MEMORY",
             "SERVER_ERROR not supported\r\n"     => "NOT_SUPPORTED",
             "SERVER_ERROR internal\r\n"          => "INTERNAL_ERROR",
             "SERVER_ERROR temporary failure\r\n" => "SERVER_TEMPORARY_ERROR");
  $self->{error} = $error_message{$reply} || "OK";
  return 0;
}

#  -----------------------------------------------------------------------
#  ------------------         BINARY PROTOCOL         --------------------
#  -----------------------------------------------------------------------

package My::Memcache::Binary;
BEGIN { @My::Memcache::Binary::ISA = qw(My::Memcache); }
use constant BINARY_HEADER_FMT => "CCnCCnNNNN";
use constant BINARY_REQUEST    => 0x80;
use constant BINARY_RESPONSE   => 0x81;

use constant BIN_CMD_GET     => 0x00;
use constant BIN_CMD_SET     => 0x01;
use constant BIN_CMD_ADD     => 0x02;
use constant BIN_CMD_REPLACE => 0x03;
use constant BIN_CMD_DELETE  => 0x04;
use constant BIN_CMD_INCR    => 0x05;
use constant BIN_CMD_DECR    => 0x06;
use constant BIN_CMD_QUIT    => 0x07;
use constant BIN_CMD_FLUSH   => 0x08;
use constant BIN_CMD_NOOP    => 0x0A;
use constant BIN_CMD_GETK    => 0x0C;
use constant BIN_CMD_GETKQ   => 0x0D;
use constant BIN_CMD_APPEND  => 0x0E;
use constant BIN_CMD_PREPEND => 0x0F;
use constant BIN_CMD_STAT    => 0x10;

sub protocol {
  return "binary";
}

sub error_message {
  my ($self, $code) = @_;
  my %error_messages = (0x00  => "OK",
                        0x01  => "NOT_FOUND",
                        0x02  => "KEY_EXISTS",
                        0x03  => "VALUE_TOO_LARGE",
                        0x04  => "INVALID_ARGUMENTS",
                        0x05  => "NOT_STORED",
                        0x06  => "NON_NUMERIC_VALUE",
                        0x07  => "NOT_MY_VBUCKET",
                        0x81  => "UNKNOWN_COMMAND",
                        0x82  => "SERVER_OUT_OF_MEMORY",
                        0x83  => "NOT_SUPPORTED",
                        0x84  => "INTERNAL_ERROR",
                        0x85  => "SERVER_BUSY",
                        0x86  => "SERVER_TEMPORARY_ERROR",
                        0x100 => "PROTOCOL_ERROR",
                        0x101 => "NETWORK_ERROR");
  return $error_messages{$code};
}

# Returns true on success, false on error
sub send_binary_request {
  my $self = shift;
  my ($cmd, $key, $val, $extra_header, $cas) = @_;

  $cas = 0 unless $cas;
  my $key_len   = length($key);
  my $val_len   = length($val);
  my $extra_len = length($extra_header);
  my $total_len = $key_len + $val_len + $extra_len;
  my $cas_hi    = ($cas >> 32) & 0xFFFFFFFF;
  my $cas_lo    = ($cas & 0xFFFFFFFF);

  $self->new_request();

  my $header = pack(BINARY_HEADER_FMT,
                    BINARY_REQUEST, $cmd,
                    $key_len, $extra_len, 0, 0, $total_len,
                    $self->{req_id}, $cas_hi, $cas_lo);
  my $packet = $header . $extra_header . $key . $val;

  return $self->write($packet);
}

sub get_binary_response {
  my $self       = shift;
  my $header_len = length(pack(BINARY_HEADER_FMT));
  my $header;
  my $body;

  $header = $self->read_known_length($header_len);
  return (0x101) if (!defined($header));

  my ($magic,  $cmd,      $key_len,  $extra_len, $datatype,
      $status, $body_len, $sequence, $cas_hi,    $cas_lo
  ) = unpack(BINARY_HEADER_FMT, $header);

  if ($magic != BINARY_RESPONSE) {
    $self->{error_detail} = "Magic number in response: $magic";
    return (0x100);
  }

  $body = $self->read_known_length($body_len);
  $self->{error} = $self->error_message($status);

  # Packet structure is: header .. extras .. key .. value
  my $cas    = ($cas_hi * (2**32)) + $cas_lo;
  my $l      = $extra_len + $key_len;
  my $extras = substr $body, 0, $extra_len;
  my $key    = substr $body, $extra_len, $key_len;
  my $value  = substr $body, $l, $body_len - $l;

  return ($status, $value, $key, $extras, $cas, $sequence);
}

sub binary_command {
  my $self = shift;
  my ($cmd, $key, $value, $extra_header, $cas) = @_;
  my $waitTime = $self->{min_wait};
  my $maxWait  = $self->{max_wait};
  my $status;
  my $wr;

  do {
    $wr = $self->send_binary_request($cmd, $key, $value, $extra_header, $cas);
    return undef unless $wr;
    ($status) = $self->get_binary_response();
    if ($status == 0x86 && $waitTime < $maxWait) {
      $self->{temp_errors} += 1;
      $self->{total_wait}  += (Time::HiRes::usleep($waitTime * 1000) / 1000);
      $waitTime *= 2;
    }
  } while ($status == 0x86 && $waitTime <= $maxWait);

  return ($status == 0) ? 1 : undef;
}

sub bin_math {
  my $self = shift;
  my ($cmd, $key, $delta, $initial) = @_;
  my $expires = 0xffffffff;    # 0xffffffff means the create flag is NOT set
  if   (defined($initial)) { $expires = $self->{exptime}; }
  else                     { $initial = 0; }
  my $value = undef;

  my $extra_header = pack "NNNNN", ($delta / (2**32)),    # delta hi
    ($delta % (2**32)),                                   # delta lo
    ($initial / (2**32)),                                 # initial hi
    ($initial % (2**32)),                                 # initial lo
    $expires;
  if ($self->send_binary_request($cmd, $key, '', $extra_header)) {
    my ($status, $packed_val) = $self->get_binary_response();
    if ($status == 0) {
      my ($val_hi, $val_lo) = unpack("NN", $packed_val);
      $value = ($val_hi * (2**32)) + $val_lo;
    }
  }
  return $value;
}

sub bin_store {
  my ($self, $cmd, $key, $value, $flags, $exptime, $cas) = @_;
  $flags   = $self->{flags}   unless $flags;
  $exptime = $self->{exptime} unless $exptime;
  my $extra_header = pack "NN", $flags, $exptime;

  return $self->binary_command($cmd, $key, $value, $extra_header, $cas);
}

## Pipelined multi-get
sub get {
  my $self = shift;
  my $idx  = $#_;              # Index of the final key
  my $cmd  = BIN_CMD_GETKQ;    # GET + KEY + NOREPLY
  my $wr;
  my $sequence = 0;
  my @results;

  for (my $i = 0 ; $i <= $idx ; $i++) {
    $cmd = BIN_CMD_GETK if ($i == $idx);    # Final request gets replies
    $wr = $self->send_binary_request($cmd, $_[$i], '', '');
  }
  return undef unless $wr;

  while ($sequence < $self->{req_id}) {
    my ($status, $value, $key, $extra, $cas);
    ($status, $value, $key, $extra, $cas, $sequence) =
      $self->get_binary_response();
    return undef if ($status > 0x01);
    if ($status == 0) {
      my $result = My::Memcache::Result->new($key, unpack("N", $extra), $cas);
      $result->{value} = $value;
      push @results, $result;
    }
  }
  $self->{get_results} = \@results;
  if (@results) {
    $self->{error} = "OK";
    return $results[0]->{value};
  }
  $self->{error} = "NOT_FOUND";
  return undef;
}

sub stats {
  my $self = shift;
  my $key  = shift;
  my %response, my $status, my $value, my $klen, my $tlen;

  $self->send_binary_request(BIN_CMD_STAT, $key, '', '');
  do {
    ($status, $value, $key) = $self->get_binary_response();
    if ($status == 0) {
      $response{$key} = $value;
    }
  } while ($status == 0 && $key);

  return %response;
}

sub flush {
  my ($self, $key, $value) = @_;
  $self->send_binary_request(BIN_CMD_FLUSH, $key, '', '');
  my ($status, $result) = $self->get_binary_response();
  return ($status == 0) ? 1 : 0;
}

sub store {
  my ($self, $cmd, $key, $value, $flags, $exptime, $cas) = @_;
  my %cmd_map = ("set"     => BIN_CMD_SET,
                 "add"     => BIN_CMD_ADD,
                 "replace" => BIN_CMD_REPLACE,
                 "append"  => BIN_CMD_APPEND,
                 "prepend" => BIN_CMD_PREPEND);
  return $self->bin_store($cmd_map{$cmd}, $key, $value, $flags, $exptime, $cas);
}

sub set {
  my ($self, $key, $value) = @_;
  return $self->bin_store(BIN_CMD_SET, $key, $value);
}

sub add {
  my ($self, $key, $value) = @_;
  return $self->bin_store(BIN_CMD_ADD, $key, $value);
}

sub replace {
  my ($self, $key, $value) = @_;
  return $self->bin_store(BIN_CMD_REPLACE, $key, $value);
}

sub append {
  my ($self, $key, $value) = @_;
  return $self->binary_command(BIN_CMD_APPEND, $key, $value, '');
}

sub prepend {
  my ($self, $key, $value) = @_;
  return $self->binary_command(BIN_CMD_PREPEND, $key, $value, '');
}

sub delete {
  my ($self, $key) = @_;
  return $self->binary_command(BIN_CMD_DELETE, $key, '', '');
}

sub incr {
  my ($self, $key, $delta, $initial) = @_;
  return $self->bin_math(BIN_CMD_INCR, $key, $delta, $initial);
}

sub decr {
  my ($self, $key, $delta, $initial) = @_;
  return $self->bin_math(BIN_CMD_DECR, $key, $delta, $initial);
}

1;
