dalli 3.0.1 → 3.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dalli might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +11 -5
- data/History.md +42 -12
- data/README.md +25 -139
- data/lib/dalli/cas/client.rb +2 -0
- data/lib/dalli/client.rb +188 -188
- data/lib/dalli/compressor.rb +13 -4
- data/lib/dalli/key_manager.rb +113 -0
- data/lib/dalli/options.rb +2 -2
- data/lib/dalli/protocol/binary/request_formatter.rb +109 -0
- data/lib/dalli/protocol/binary/response_processor.rb +149 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +57 -0
- data/lib/dalli/protocol/binary.rb +274 -483
- data/lib/dalli/protocol/server_config_parser.rb +84 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +2 -3
- data/lib/dalli/ring.rb +91 -35
- data/lib/dalli/server.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +98 -45
- data/lib/dalli/version.rb +3 -1
- data/lib/dalli.rb +31 -11
- data/lib/rack/session/dalli.rb +28 -18
- metadata +60 -7
| @@ -1,20 +1,29 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require  | 
| 4 | 
            -
            require  | 
| 3 | 
            +
            require 'English'
         | 
| 4 | 
            +
            require 'forwardable'
         | 
| 5 | 
            +
            require 'socket'
         | 
| 6 | 
            +
            require 'timeout'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require_relative 'binary/request_formatter'
         | 
| 9 | 
            +
            require_relative 'binary/response_processor'
         | 
| 10 | 
            +
            require_relative 'binary/sasl_authentication'
         | 
| 5 11 |  | 
| 6 12 | 
             
            module Dalli
         | 
| 7 13 | 
             
              module Protocol
         | 
| 14 | 
            +
                ##
         | 
| 15 | 
            +
                # Access point for a single Memcached server, accessed via Memcached's binary
         | 
| 16 | 
            +
                # protocol.  Contains logic for managing connection state to the server (retries, etc),
         | 
| 17 | 
            +
                # formatting requests to the server, and unpacking responses.
         | 
| 18 | 
            +
                ##
         | 
| 8 19 | 
             
                class Binary
         | 
| 9 | 
            -
                   | 
| 10 | 
            -
             | 
| 11 | 
            -
                  attr_accessor :weight
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
             | 
| 14 | 
            -
                   | 
| 15 | 
            -
             | 
| 16 | 
            -
                  DEFAULT_PORT = 11211
         | 
| 17 | 
            -
                  DEFAULT_WEIGHT = 1
         | 
| 20 | 
            +
                  extend Forwardable
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  attr_accessor :hostname, :port, :weight, :options
         | 
| 23 | 
            +
                  attr_reader :sock, :socket_type
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
         | 
| 26 | 
            +
             | 
| 18 27 | 
             
                  DEFAULTS = {
         | 
| 19 28 | 
             
                    # seconds between trying to contact a remote server
         | 
| 20 29 | 
             
                    down_retry_delay: 30,
         | 
| @@ -24,33 +33,20 @@ module Dalli | |
| 24 33 | 
             
                    socket_max_failures: 2,
         | 
| 25 34 | 
             
                    # amount of time to sleep between retries when a failure occurs
         | 
| 26 35 | 
             
                    socket_failure_delay: 0.1,
         | 
| 27 | 
            -
                    # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
         | 
| 28 | 
            -
                    value_max_bytes: 1024 * 1024,
         | 
| 29 | 
            -
                    compress: true,
         | 
| 30 | 
            -
                    compressor: Compressor,
         | 
| 31 | 
            -
                    # min byte size to attempt compression
         | 
| 32 | 
            -
                    compression_min_size: 4 * 1024,
         | 
| 33 | 
            -
                    serializer: Marshal,
         | 
| 34 36 | 
             
                    username: nil,
         | 
| 35 | 
            -
                    password: nil | 
| 36 | 
            -
             | 
| 37 | 
            -
                    # max byte size for SO_SNDBUF
         | 
| 38 | 
            -
                    sndbuf: nil,
         | 
| 39 | 
            -
                    # max byte size for SO_RCVBUF
         | 
| 40 | 
            -
                    rcvbuf: nil
         | 
| 41 | 
            -
                  }
         | 
| 37 | 
            +
                    password: nil
         | 
| 38 | 
            +
                  }.freeze
         | 
| 42 39 |  | 
| 43 40 | 
             
                  def initialize(attribs, options = {})
         | 
| 44 | 
            -
                    @hostname, @port, @weight, @socket_type =  | 
| 45 | 
            -
                    @fail_count = 0
         | 
| 46 | 
            -
                    @down_at = nil
         | 
| 47 | 
            -
                    @last_down_at = nil
         | 
| 41 | 
            +
                    @hostname, @port, @weight, @socket_type, options = ServerConfigParser.parse(attribs, options)
         | 
| 48 42 | 
             
                    @options = DEFAULTS.merge(options)
         | 
| 43 | 
            +
                    @value_marshaller = ValueMarshaller.new(@options)
         | 
| 44 | 
            +
                    @response_processor = ResponseProcessor.new(self, @value_marshaller)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    reset_down_info
         | 
| 49 47 | 
             
                    @sock = nil
         | 
| 50 | 
            -
                    @msg = nil
         | 
| 51 | 
            -
                    @error = nil
         | 
| 52 48 | 
             
                    @pid = nil
         | 
| 53 | 
            -
                    @ | 
| 49 | 
            +
                    @request_in_progress = false
         | 
| 54 50 | 
             
                  end
         | 
| 55 51 |  | 
| 56 52 | 
             
                  def name
         | 
| @@ -61,33 +57,50 @@ module Dalli | |
| 61 57 | 
             
                    end
         | 
| 62 58 | 
             
                  end
         | 
| 63 59 |  | 
| 64 | 
            -
                  # Chokepoint method for  | 
| 65 | 
            -
                  def request( | 
| 60 | 
            +
                  # Chokepoint method for error handling and ensuring liveness
         | 
| 61 | 
            +
                  def request(opcode, *args)
         | 
| 66 62 | 
             
                    verify_state
         | 
| 67 | 
            -
                     | 
| 63 | 
            +
                    # The alive? call has the side effect of connecting the underlying
         | 
| 64 | 
            +
                    # socket if it is not connected, or there's been a disconnect
         | 
| 65 | 
            +
                    # because of timeout or other error.  Method raises an error
         | 
| 66 | 
            +
                    # if it can't connect
         | 
| 67 | 
            +
                    raise_memcached_down_err unless alive?
         | 
| 68 | 
            +
             | 
| 68 69 | 
             
                    begin
         | 
| 69 | 
            -
                      send( | 
| 70 | 
            -
                    rescue Dalli::MarshalError =>  | 
| 71 | 
            -
                       | 
| 72 | 
            -
                      Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
         | 
| 70 | 
            +
                      send(opcode, *args)
         | 
| 71 | 
            +
                    rescue Dalli::MarshalError => e
         | 
| 72 | 
            +
                      log_marshall_err(args.first, e)
         | 
| 73 73 | 
             
                      raise
         | 
| 74 74 | 
             
                    rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
         | 
| 75 75 | 
             
                      raise
         | 
| 76 | 
            -
                    rescue =>  | 
| 77 | 
            -
                       | 
| 78 | 
            -
                      Dalli.logger.error ex.backtrace.join("\n\t")
         | 
| 76 | 
            +
                    rescue StandardError => e
         | 
| 77 | 
            +
                      log_unexpected_err(e)
         | 
| 79 78 | 
             
                      down!
         | 
| 80 79 | 
             
                    end
         | 
| 81 80 | 
             
                  end
         | 
| 82 81 |  | 
| 82 | 
            +
                  def raise_memcached_down_err
         | 
| 83 | 
            +
                    raise Dalli::NetworkError,
         | 
| 84 | 
            +
                          "#{name} is down: #{@error} #{@msg}. If you are sure it is running, "\
         | 
| 85 | 
            +
                          "ensure memcached version is > #{::Dalli::MIN_SUPPORTED_MEMCACHED_VERSION}."
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def log_marshall_err(key, err)
         | 
| 89 | 
            +
                    Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
         | 
| 90 | 
            +
                    Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  def log_unexpected_err(err)
         | 
| 94 | 
            +
                    Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
         | 
| 95 | 
            +
                    Dalli.logger.error err.backtrace.join("\n\t")
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  # The socket connection to the underlying server is initialized as a side
         | 
| 99 | 
            +
                  # effect of this call.  In fact, this is the ONLY place where that
         | 
| 100 | 
            +
                  # socket connection is initialized.
         | 
| 83 101 | 
             
                  def alive?
         | 
| 84 102 | 
             
                    return true if @sock
         | 
| 85 | 
            -
             | 
| 86 | 
            -
                    if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
         | 
| 87 | 
            -
                      time = @last_down_at + options[:down_retry_delay] - Time.now
         | 
| 88 | 
            -
                      Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
         | 
| 89 | 
            -
                      return false
         | 
| 90 | 
            -
                    end
         | 
| 103 | 
            +
                    return false unless reconnect_down_server?
         | 
| 91 104 |  | 
| 92 105 | 
             
                    connect
         | 
| 93 106 | 
             
                    !!@sock
         | 
| @@ -95,31 +108,37 @@ module Dalli | |
| 95 108 | 
             
                    false
         | 
| 96 109 | 
             
                  end
         | 
| 97 110 |  | 
| 111 | 
            +
                  def reconnect_down_server?
         | 
| 112 | 
            +
                    return true unless @last_down_at
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                    time_to_next_reconnect = @last_down_at + options[:down_retry_delay] - Time.now
         | 
| 115 | 
            +
                    return true unless time_to_next_reconnect.positive?
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    Dalli.logger.debug do
         | 
| 118 | 
            +
                      format('down_retry_delay not reached for %<name>s (%<time>.3f seconds left)', name: name,
         | 
| 119 | 
            +
                                                                                                    time: time_to_next_reconnect)
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                    false
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  # Closes the underlying socket and cleans up
         | 
| 125 | 
            +
                  # socket state.
         | 
| 98 126 | 
             
                  def close
         | 
| 99 127 | 
             
                    return unless @sock
         | 
| 128 | 
            +
             | 
| 100 129 | 
             
                    begin
         | 
| 101 130 | 
             
                      @sock.close
         | 
| 102 | 
            -
                    rescue
         | 
| 131 | 
            +
                    rescue StandardError
         | 
| 103 132 | 
             
                      nil
         | 
| 104 133 | 
             
                    end
         | 
| 105 134 | 
             
                    @sock = nil
         | 
| 106 135 | 
             
                    @pid = nil
         | 
| 107 | 
            -
                     | 
| 108 | 
            -
                  end
         | 
| 109 | 
            -
             | 
| 110 | 
            -
                  def lock!
         | 
| 136 | 
            +
                    abort_request!
         | 
| 111 137 | 
             
                  end
         | 
| 112 138 |  | 
| 113 | 
            -
                  def  | 
| 114 | 
            -
                  end
         | 
| 139 | 
            +
                  def lock!; end
         | 
| 115 140 |  | 
| 116 | 
            -
                  def  | 
| 117 | 
            -
                    @options[:serializer]
         | 
| 118 | 
            -
                  end
         | 
| 119 | 
            -
             | 
| 120 | 
            -
                  def compressor
         | 
| 121 | 
            -
                    @options[:compressor]
         | 
| 122 | 
            -
                  end
         | 
| 141 | 
            +
                  def unlock!; end
         | 
| 123 142 |  | 
| 124 143 | 
             
                  # Start reading key/value pairs from this connection. This is usually called
         | 
| 125 144 | 
             
                  # after a series of GETKQ commands. A NOOP is sent, and the server begins
         | 
| @@ -129,9 +148,9 @@ module Dalli | |
| 129 148 | 
             
                  def multi_response_start
         | 
| 130 149 | 
             
                    verify_state
         | 
| 131 150 | 
             
                    write_noop
         | 
| 132 | 
            -
                    @multi_buffer = + | 
| 151 | 
            +
                    @multi_buffer = +''
         | 
| 133 152 | 
             
                    @position = 0
         | 
| 134 | 
            -
                     | 
| 153 | 
            +
                    start_request!
         | 
| 135 154 | 
             
                  end
         | 
| 136 155 |  | 
| 137 156 | 
             
                  # Did the last call to #multi_response_start complete successfully?
         | 
| @@ -146,41 +165,39 @@ module Dalli | |
| 146 165 | 
             
                  #
         | 
| 147 166 | 
             
                  # Returns a Hash of kv pairs received.
         | 
| 148 167 | 
             
                  def multi_response_nonblock
         | 
| 149 | 
            -
                    reconnect!  | 
| 168 | 
            +
                    reconnect! 'multi_response has completed' if @multi_buffer.nil?
         | 
| 150 169 |  | 
| 151 170 | 
             
                    @multi_buffer << @sock.read_available
         | 
| 152 171 | 
             
                    buf = @multi_buffer
         | 
| 153 172 | 
             
                    pos = @position
         | 
| 154 173 | 
             
                    values = {}
         | 
| 155 174 |  | 
| 156 | 
            -
                    while buf.bytesize - pos >=  | 
| 157 | 
            -
                      header = buf.slice(pos,  | 
| 158 | 
            -
                       | 
| 175 | 
            +
                    while buf.bytesize - pos >= ResponseProcessor::RESP_HEADER_SIZE
         | 
| 176 | 
            +
                      header = buf.slice(pos, ResponseProcessor::RESP_HEADER_SIZE)
         | 
| 177 | 
            +
                      _, extra_len, key_len, body_len, cas = @response_processor.unpack_header(header)
         | 
| 159 178 |  | 
| 160 | 
            -
                       | 
| 161 | 
            -
             | 
| 162 | 
            -
                         | 
| 163 | 
            -
                        @position = nil
         | 
| 164 | 
            -
                        @inprogress = false
         | 
| 179 | 
            +
                      # We've reached the noop at the end of the pipeline
         | 
| 180 | 
            +
                      if key_len.zero?
         | 
| 181 | 
            +
                        finish_multi_response
         | 
| 165 182 | 
             
                        break
         | 
| 183 | 
            +
                      end
         | 
| 166 184 |  | 
| 167 | 
            -
                       | 
| 168 | 
            -
             | 
| 169 | 
            -
             | 
| 170 | 
            -
                        value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0
         | 
| 171 | 
            -
             | 
| 172 | 
            -
                        pos = pos + 24 + body_length
         | 
| 173 | 
            -
             | 
| 174 | 
            -
                        begin
         | 
| 175 | 
            -
                          values[key] = [deserialize(value, flags), cas]
         | 
| 176 | 
            -
                        rescue DalliError
         | 
| 177 | 
            -
                        end
         | 
| 185 | 
            +
                      # Break and read more unless we already have the entire response for this header
         | 
| 186 | 
            +
                      resp_size = ResponseProcessor::RESP_HEADER_SIZE + body_len
         | 
| 187 | 
            +
                      break unless buf.bytesize - pos >= resp_size
         | 
| 178 188 |  | 
| 179 | 
            -
                       | 
| 180 | 
            -
             | 
| 181 | 
            -
                         | 
| 189 | 
            +
                      body = buf.slice(pos + ResponseProcessor::RESP_HEADER_SIZE, body_len)
         | 
| 190 | 
            +
                      begin
         | 
| 191 | 
            +
                        key, value = @response_processor.unpack_response_body(extra_len, key_len, body, true)
         | 
| 192 | 
            +
                        values[key] = [value, cas]
         | 
| 193 | 
            +
                      rescue DalliError
         | 
| 194 | 
            +
                        # TODO: Determine if we should be swallowing
         | 
| 195 | 
            +
                        # this error
         | 
| 182 196 | 
             
                      end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                      pos = pos + ResponseProcessor::RESP_HEADER_SIZE + body_len
         | 
| 183 199 | 
             
                    end
         | 
| 200 | 
            +
                    # TODO: We should be discarding the already processed buffer at this point
         | 
| 184 201 | 
             
                    @position = pos
         | 
| 185 202 |  | 
| 186 203 | 
             
                    values
         | 
| @@ -188,6 +205,12 @@ module Dalli | |
| 188 205 | 
             
                    failure!(e)
         | 
| 189 206 | 
             
                  end
         | 
| 190 207 |  | 
| 208 | 
            +
                  def finish_multi_response
         | 
| 209 | 
            +
                    @multi_buffer = nil
         | 
| 210 | 
            +
                    @position = nil
         | 
| 211 | 
            +
                    finish_request!
         | 
| 212 | 
            +
                  end
         | 
| 213 | 
            +
             | 
| 191 214 | 
             
                  # Abort an earlier #multi_response_start. Used to signal an external
         | 
| 192 215 | 
             
                  # timeout. The underlying socket is disconnected, and the exception is
         | 
| 193 216 | 
             
                  # swallowed.
         | 
| @@ -196,31 +219,81 @@ module Dalli | |
| 196 219 | 
             
                  def multi_response_abort
         | 
| 197 220 | 
             
                    @multi_buffer = nil
         | 
| 198 221 | 
             
                    @position = nil
         | 
| 199 | 
            -
                     | 
| 200 | 
            -
                     | 
| 222 | 
            +
                    abort_request!
         | 
| 223 | 
            +
                    return true unless @sock
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                    failure!(RuntimeError.new('External timeout'))
         | 
| 201 226 | 
             
                  rescue NetworkError
         | 
| 202 227 | 
             
                    true
         | 
| 203 228 | 
             
                  end
         | 
| 204 229 |  | 
| 230 | 
            +
                  def read(count)
         | 
| 231 | 
            +
                    start_request!
         | 
| 232 | 
            +
                    data = @sock.readfull(count)
         | 
| 233 | 
            +
                    finish_request!
         | 
| 234 | 
            +
                    data
         | 
| 235 | 
            +
                  rescue SystemCallError, Timeout::Error, EOFError => e
         | 
| 236 | 
            +
                    failure!(e)
         | 
| 237 | 
            +
                  end
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                  def write(bytes)
         | 
| 240 | 
            +
                    start_request!
         | 
| 241 | 
            +
                    result = @sock.write(bytes)
         | 
| 242 | 
            +
                    finish_request!
         | 
| 243 | 
            +
                    result
         | 
| 244 | 
            +
                  rescue SystemCallError, Timeout::Error => e
         | 
| 245 | 
            +
                    failure!(e)
         | 
| 246 | 
            +
                  end
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                  def socket_timeout
         | 
| 249 | 
            +
                    @socket_timeout ||= @options[:socket_timeout]
         | 
| 250 | 
            +
                  end
         | 
| 251 | 
            +
             | 
| 205 252 | 
             
                  # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
         | 
| 206 253 |  | 
| 207 254 | 
             
                  private
         | 
| 208 255 |  | 
| 256 | 
            +
                  def request_in_progress?
         | 
| 257 | 
            +
                    @request_in_progress
         | 
| 258 | 
            +
                  end
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                  def start_request!
         | 
| 261 | 
            +
                    @request_in_progress = true
         | 
| 262 | 
            +
                  end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                  def finish_request!
         | 
| 265 | 
            +
                    @request_in_progress = false
         | 
| 266 | 
            +
                  end
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                  def abort_request!
         | 
| 269 | 
            +
                    @request_in_progress = false
         | 
| 270 | 
            +
                  end
         | 
| 271 | 
            +
             | 
| 209 272 | 
             
                  def verify_state
         | 
| 210 | 
            -
                    failure!(RuntimeError.new( | 
| 211 | 
            -
                    if  | 
| 212 | 
            -
             | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
                     | 
| 273 | 
            +
                    failure!(RuntimeError.new('Already writing to socket')) if request_in_progress?
         | 
| 274 | 
            +
                    reconnect_on_fork if fork_detected?
         | 
| 275 | 
            +
                  end
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                  def fork_detected?
         | 
| 278 | 
            +
                    @pid && @pid != Process.pid
         | 
| 216 279 | 
             
                  end
         | 
| 217 280 |  | 
| 281 | 
            +
                  def reconnect_on_fork
         | 
| 282 | 
            +
                    message = 'Fork detected, re-connecting child process...'
         | 
| 283 | 
            +
                    Dalli.logger.info { message }
         | 
| 284 | 
            +
                    reconnect! message
         | 
| 285 | 
            +
                  end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                  # Marks the server instance as needing reconnect.  Raises a
         | 
| 288 | 
            +
                  # Dalli::NetworkError with the specified message.  Calls close
         | 
| 289 | 
            +
                  # to clean up socket state
         | 
| 218 290 | 
             
                  def reconnect!(message)
         | 
| 219 291 | 
             
                    close
         | 
| 220 292 | 
             
                    sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
         | 
| 221 293 | 
             
                    raise Dalli::NetworkError, message
         | 
| 222 294 | 
             
                  end
         | 
| 223 295 |  | 
| 296 | 
            +
                  # Raises Dalli::NetworkError
         | 
| 224 297 | 
             
                  def failure!(exception)
         | 
| 225 298 | 
             
                    message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
         | 
| 226 299 | 
             
                    Dalli.logger.warn { message }
         | 
| @@ -229,34 +302,47 @@ module Dalli | |
| 229 302 | 
             
                    if @fail_count >= options[:socket_max_failures]
         | 
| 230 303 | 
             
                      down!
         | 
| 231 304 | 
             
                    else
         | 
| 232 | 
            -
                      reconnect!  | 
| 305 | 
            +
                      reconnect! 'Socket operation failed, retrying...'
         | 
| 233 306 | 
             
                    end
         | 
| 234 307 | 
             
                  end
         | 
| 235 308 |  | 
| 309 | 
            +
                  # Marks the server instance as down.  Updates the down_at state
         | 
| 310 | 
            +
                  # and raises an Dalli::NetworkError that includes the underlying
         | 
| 311 | 
            +
                  # error in the message.  Calls close to clean up socket state
         | 
| 236 312 | 
             
                  def down!
         | 
| 237 313 | 
             
                    close
         | 
| 314 | 
            +
                    log_down_detected
         | 
| 315 | 
            +
             | 
| 316 | 
            +
                    @error = $ERROR_INFO&.class&.name
         | 
| 317 | 
            +
                    @msg ||= $ERROR_INFO&.message
         | 
| 318 | 
            +
                    raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
         | 
| 319 | 
            +
                  end
         | 
| 238 320 |  | 
| 321 | 
            +
                  def log_down_detected
         | 
| 239 322 | 
             
                    @last_down_at = Time.now
         | 
| 240 323 |  | 
| 241 324 | 
             
                    if @down_at
         | 
| 242 325 | 
             
                      time = Time.now - @down_at
         | 
| 243 | 
            -
                      Dalli.logger.debug {  | 
| 326 | 
            +
                      Dalli.logger.debug { format('%<name>s is still down (for %<time>.3f seconds now)', name: name, time: time) }
         | 
| 244 327 | 
             
                    else
         | 
| 245 328 | 
             
                      @down_at = @last_down_at
         | 
| 246 | 
            -
                      Dalli.logger.warn | 
| 329 | 
            +
                      Dalli.logger.warn("#{name} is down")
         | 
| 247 330 | 
             
                    end
         | 
| 331 | 
            +
                  end
         | 
| 248 332 |  | 
| 249 | 
            -
             | 
| 250 | 
            -
                     | 
| 251 | 
            -
             | 
| 333 | 
            +
                  def log_up_detected
         | 
| 334 | 
            +
                    return unless @down_at
         | 
| 335 | 
            +
             | 
| 336 | 
            +
                    time = Time.now - @down_at
         | 
| 337 | 
            +
                    Dalli.logger.warn { format('%<name>s is back (downtime was %<time>.3f seconds)', name: name, time: time) }
         | 
| 252 338 | 
             
                  end
         | 
| 253 339 |  | 
| 254 340 | 
             
                  def up!
         | 
| 255 | 
            -
                     | 
| 256 | 
            -
             | 
| 257 | 
            -
             | 
| 258 | 
            -
                    end
         | 
| 341 | 
            +
                    log_up_detected
         | 
| 342 | 
            +
                    reset_down_info
         | 
| 343 | 
            +
                  end
         | 
| 259 344 |  | 
| 345 | 
            +
                  def reset_down_info
         | 
| 260 346 | 
             
                    @fail_count = 0
         | 
| 261 347 | 
             
                    @down_at = nil
         | 
| 262 348 | 
             
                    @last_down_at = nil
         | 
| @@ -268,96 +354,103 @@ module Dalli | |
| 268 354 | 
             
                    Thread.current[:dalli_multi]
         | 
| 269 355 | 
             
                  end
         | 
| 270 356 |  | 
| 357 | 
            +
                  def cache_nils?(opts)
         | 
| 358 | 
            +
                    return false unless opts.is_a?(Hash)
         | 
| 359 | 
            +
             | 
| 360 | 
            +
                    opts[:cache_nils] ? true : false
         | 
| 361 | 
            +
                  end
         | 
| 362 | 
            +
             | 
| 271 363 | 
             
                  def get(key, options = nil)
         | 
| 272 | 
            -
                    req =  | 
| 364 | 
            +
                    req = RequestFormatter.standard_request(opkey: :get, key: key)
         | 
| 273 365 | 
             
                    write(req)
         | 
| 274 | 
            -
                    generic_response(true,  | 
| 366 | 
            +
                    @response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
         | 
| 275 367 | 
             
                  end
         | 
| 276 368 |  | 
| 277 369 | 
             
                  def send_multiget(keys)
         | 
| 278 | 
            -
                    req = + | 
| 370 | 
            +
                    req = +''
         | 
| 279 371 | 
             
                    keys.each do |key|
         | 
| 280 | 
            -
                      req <<  | 
| 372 | 
            +
                      req << RequestFormatter.standard_request(opkey: :getkq, key: key)
         | 
| 281 373 | 
             
                    end
         | 
| 282 374 | 
             
                    # Could send noop here instead of in multi_response_start
         | 
| 283 375 | 
             
                    write(req)
         | 
| 284 376 | 
             
                  end
         | 
| 285 377 |  | 
| 286 378 | 
             
                  def set(key, value, ttl, cas, options)
         | 
| 287 | 
            -
                     | 
| 288 | 
            -
                    ttl  | 
| 289 | 
            -
             | 
| 290 | 
            -
                    guard_max_value(key, value)
         | 
| 291 | 
            -
             | 
| 292 | 
            -
                    req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:set])
         | 
| 293 | 
            -
                    write(req)
         | 
| 294 | 
            -
                    cas_response unless multi?
         | 
| 379 | 
            +
                    opkey = multi? ? :setq : :set
         | 
| 380 | 
            +
                    process_value_req(opkey, key, value, ttl, cas, options)
         | 
| 295 381 | 
             
                  end
         | 
| 296 382 |  | 
| 297 383 | 
             
                  def add(key, value, ttl, options)
         | 
| 298 | 
            -
                     | 
| 299 | 
            -
                     | 
| 300 | 
            -
             | 
| 301 | 
            -
                    guard_max_value(key, value)
         | 
| 302 | 
            -
             | 
| 303 | 
            -
                    req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:add])
         | 
| 304 | 
            -
                    write(req)
         | 
| 305 | 
            -
                    cas_response unless multi?
         | 
| 384 | 
            +
                    opkey = multi? ? :addq : :add
         | 
| 385 | 
            +
                    cas = 0
         | 
| 386 | 
            +
                    process_value_req(opkey, key, value, ttl, cas, options)
         | 
| 306 387 | 
             
                  end
         | 
| 307 388 |  | 
| 308 389 | 
             
                  def replace(key, value, ttl, cas, options)
         | 
| 309 | 
            -
                     | 
| 310 | 
            -
                    ttl  | 
| 390 | 
            +
                    opkey = multi? ? :replaceq : :replace
         | 
| 391 | 
            +
                    process_value_req(opkey, key, value, ttl, cas, options)
         | 
| 392 | 
            +
                  end
         | 
| 311 393 |  | 
| 312 | 
            -
             | 
| 394 | 
            +
                  # rubocop:disable Metrics/ParameterLists
         | 
| 395 | 
            +
                  def process_value_req(opkey, key, value, ttl, cas, options)
         | 
| 396 | 
            +
                    (value, bitflags) = @value_marshaller.store(key, value, options)
         | 
| 397 | 
            +
                    ttl = TtlSanitizer.sanitize(ttl)
         | 
| 313 398 |  | 
| 314 | 
            -
                    req =  | 
| 399 | 
            +
                    req = RequestFormatter.standard_request(opkey: opkey, key: key,
         | 
| 400 | 
            +
                                                            value: value, bitflags: bitflags,
         | 
| 401 | 
            +
                                                            ttl: ttl, cas: cas)
         | 
| 315 402 | 
             
                    write(req)
         | 
| 316 | 
            -
                    cas_response unless multi?
         | 
| 403 | 
            +
                    @response_processor.cas_response unless multi?
         | 
| 317 404 | 
             
                  end
         | 
| 405 | 
            +
                  # rubocop:enable Metrics/ParameterLists
         | 
| 318 406 |  | 
| 319 407 | 
             
                  def delete(key, cas)
         | 
| 320 | 
            -
                     | 
| 408 | 
            +
                    opkey = multi? ? :deleteq : :delete
         | 
| 409 | 
            +
                    req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
         | 
| 321 410 | 
             
                    write(req)
         | 
| 322 | 
            -
                    generic_response unless multi?
         | 
| 411 | 
            +
                    @response_processor.generic_response unless multi?
         | 
| 323 412 | 
             
                  end
         | 
| 324 413 |  | 
| 325 | 
            -
                  def flush(ttl)
         | 
| 326 | 
            -
                    req =  | 
| 414 | 
            +
                  def flush(ttl = 0)
         | 
| 415 | 
            +
                    req = RequestFormatter.standard_request(opkey: :flush, ttl: ttl)
         | 
| 327 416 | 
             
                    write(req)
         | 
| 328 | 
            -
                    generic_response
         | 
| 417 | 
            +
                    @response_processor.generic_response
         | 
| 329 418 | 
             
                  end
         | 
| 330 419 |  | 
| 331 | 
            -
                   | 
| 332 | 
            -
             | 
| 333 | 
            -
             | 
| 334 | 
            -
             | 
| 335 | 
            -
             | 
| 336 | 
            -
             | 
| 337 | 
            -
             | 
| 338 | 
            -
             | 
| 339 | 
            -
                     | 
| 420 | 
            +
                  # This allows us to special case a nil initial value, and
         | 
| 421 | 
            +
                  # handle it differently than a zero.  This special value
         | 
| 422 | 
            +
                  # for expiry causes memcached to return a not found
         | 
| 423 | 
            +
                  # if the key doesn't already exist, rather than
         | 
| 424 | 
            +
                  # setting the initial value
         | 
| 425 | 
            +
                  NOT_FOUND_EXPIRY = 0xFFFFFFFF
         | 
| 426 | 
            +
             | 
| 427 | 
            +
                  def decr_incr(opkey, key, count, ttl, initial)
         | 
| 428 | 
            +
                    expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
         | 
| 429 | 
            +
                    initial ||= 0
         | 
| 430 | 
            +
                    write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
         | 
| 431 | 
            +
                                                             count: count, initial: initial, expiry: expiry))
         | 
| 432 | 
            +
                    @response_processor.decr_incr_response
         | 
| 340 433 | 
             
                  end
         | 
| 341 434 |  | 
| 342 | 
            -
                  def decr(key, count, ttl,  | 
| 343 | 
            -
                    decr_incr :decr, key, count, ttl,  | 
| 435 | 
            +
                  def decr(key, count, ttl, initial)
         | 
| 436 | 
            +
                    decr_incr :decr, key, count, ttl, initial
         | 
| 344 437 | 
             
                  end
         | 
| 345 438 |  | 
| 346 | 
            -
                  def incr(key, count, ttl,  | 
| 347 | 
            -
                    decr_incr :incr, key, count, ttl,  | 
| 439 | 
            +
                  def incr(key, count, ttl, initial)
         | 
| 440 | 
            +
                    decr_incr :incr, key, count, ttl, initial
         | 
| 348 441 | 
             
                  end
         | 
| 349 442 |  | 
| 350 | 
            -
                  def write_append_prepend( | 
| 351 | 
            -
                    write_generic  | 
| 443 | 
            +
                  def write_append_prepend(opkey, key, value)
         | 
| 444 | 
            +
                    write_generic RequestFormatter.standard_request(opkey: opkey, key: key, value: value)
         | 
| 352 445 | 
             
                  end
         | 
| 353 446 |  | 
| 354 447 | 
             
                  def write_generic(bytes)
         | 
| 355 448 | 
             
                    write(bytes)
         | 
| 356 | 
            -
                    generic_response
         | 
| 449 | 
            +
                    @response_processor.generic_response
         | 
| 357 450 | 
             
                  end
         | 
| 358 451 |  | 
| 359 452 | 
             
                  def write_noop
         | 
| 360 | 
            -
                    req =  | 
| 453 | 
            +
                    req = RequestFormatter.standard_request(opkey: :noop)
         | 
| 361 454 | 
             
                    write(req)
         | 
| 362 455 | 
             
                  end
         | 
| 363 456 |  | 
| @@ -365,7 +458,7 @@ module Dalli | |
| 365 458 | 
             
                  # We need to read all the responses at once.
         | 
| 366 459 | 
             
                  def noop
         | 
| 367 460 | 
             
                    write_noop
         | 
| 368 | 
            -
                     | 
| 461 | 
            +
                    @response_processor.multi_with_keys_response
         | 
| 369 462 | 
             
                  end
         | 
| 370 463 |  | 
| 371 464 | 
             
                  def append(key, value)
         | 
| @@ -376,209 +469,36 @@ module Dalli | |
| 376 469 | 
             
                    write_append_prepend :prepend, key, value
         | 
| 377 470 | 
             
                  end
         | 
| 378 471 |  | 
| 379 | 
            -
                  def stats(info =  | 
| 380 | 
            -
                    req =  | 
| 472 | 
            +
                  def stats(info = '')
         | 
| 473 | 
            +
                    req = RequestFormatter.standard_request(opkey: :stat, key: info)
         | 
| 381 474 | 
             
                    write(req)
         | 
| 382 | 
            -
                     | 
| 475 | 
            +
                    @response_processor.multi_with_keys_response
         | 
| 383 476 | 
             
                  end
         | 
| 384 477 |  | 
| 385 478 | 
             
                  def reset_stats
         | 
| 386 | 
            -
                    write_generic  | 
| 479 | 
            +
                    write_generic RequestFormatter.standard_request(opkey: :stat, key: 'reset')
         | 
| 387 480 | 
             
                  end
         | 
| 388 481 |  | 
| 389 482 | 
             
                  def cas(key)
         | 
| 390 | 
            -
                    req =  | 
| 483 | 
            +
                    req = RequestFormatter.standard_request(opkey: :get, key: key)
         | 
| 391 484 | 
             
                    write(req)
         | 
| 392 | 
            -
                    data_cas_response
         | 
| 485 | 
            +
                    @response_processor.data_cas_response
         | 
| 393 486 | 
             
                  end
         | 
| 394 487 |  | 
| 395 488 | 
             
                  def version
         | 
| 396 | 
            -
                    write_generic  | 
| 489 | 
            +
                    write_generic RequestFormatter.standard_request(opkey: :version)
         | 
| 397 490 | 
             
                  end
         | 
| 398 491 |  | 
| 399 492 | 
             
                  def touch(key, ttl)
         | 
| 400 | 
            -
                    ttl =  | 
| 401 | 
            -
                    write_generic  | 
| 493 | 
            +
                    ttl = TtlSanitizer.sanitize(ttl)
         | 
| 494 | 
            +
                    write_generic RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl)
         | 
| 402 495 | 
             
                  end
         | 
| 403 496 |  | 
| 404 497 | 
             
                  def gat(key, ttl, options = nil)
         | 
| 405 | 
            -
                    ttl =  | 
| 406 | 
            -
                    req =  | 
| 498 | 
            +
                    ttl = TtlSanitizer.sanitize(ttl)
         | 
| 499 | 
            +
                    req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
         | 
| 407 500 | 
             
                    write(req)
         | 
| 408 | 
            -
                    generic_response(true,  | 
| 409 | 
            -
                  end
         | 
| 410 | 
            -
             | 
| 411 | 
            -
                  # http://www.hjp.at/zettel/m/memcached_flags.rxml
         | 
| 412 | 
            -
                  # Looks like most clients use bit 0 to indicate native language serialization
         | 
| 413 | 
            -
                  # and bit 1 to indicate gzip compression.
         | 
| 414 | 
            -
                  FLAG_SERIALIZED = 0x1
         | 
| 415 | 
            -
                  FLAG_COMPRESSED = 0x2
         | 
| 416 | 
            -
             | 
| 417 | 
            -
                  def serialize(key, value, options = nil)
         | 
| 418 | 
            -
                    marshalled = false
         | 
| 419 | 
            -
                    value = if options && options[:raw]
         | 
| 420 | 
            -
                      value.to_s
         | 
| 421 | 
            -
                    else
         | 
| 422 | 
            -
                      marshalled = true
         | 
| 423 | 
            -
                      begin
         | 
| 424 | 
            -
                        serializer.dump(value)
         | 
| 425 | 
            -
                      rescue Timeout::Error => e
         | 
| 426 | 
            -
                        raise e
         | 
| 427 | 
            -
                      rescue => ex
         | 
| 428 | 
            -
                        # Marshalling can throw several different types of generic Ruby exceptions.
         | 
| 429 | 
            -
                        # Convert to a specific exception so we can special case it higher up the stack.
         | 
| 430 | 
            -
                        exc = Dalli::MarshalError.new(ex.message)
         | 
| 431 | 
            -
                        exc.set_backtrace ex.backtrace
         | 
| 432 | 
            -
                        raise exc
         | 
| 433 | 
            -
                      end
         | 
| 434 | 
            -
                    end
         | 
| 435 | 
            -
                    compressed = false
         | 
| 436 | 
            -
                    set_compress_option = true if options && options[:compress]
         | 
| 437 | 
            -
                    if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size]
         | 
| 438 | 
            -
                      value = compressor.compress(value)
         | 
| 439 | 
            -
                      compressed = true
         | 
| 440 | 
            -
                    end
         | 
| 441 | 
            -
             | 
| 442 | 
            -
                    flags = 0
         | 
| 443 | 
            -
                    flags |= FLAG_COMPRESSED if compressed
         | 
| 444 | 
            -
                    flags |= FLAG_SERIALIZED if marshalled
         | 
| 445 | 
            -
                    [value, flags]
         | 
| 446 | 
            -
                  end
         | 
| 447 | 
            -
             | 
| 448 | 
            -
                  def deserialize(value, flags)
         | 
| 449 | 
            -
                    value = compressor.decompress(value) if (flags & FLAG_COMPRESSED) != 0
         | 
| 450 | 
            -
                    value = serializer.load(value) if (flags & FLAG_SERIALIZED) != 0
         | 
| 451 | 
            -
                    value
         | 
| 452 | 
            -
                  rescue TypeError
         | 
| 453 | 
            -
                    raise unless /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/.match?($!.message)
         | 
| 454 | 
            -
                    raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
         | 
| 455 | 
            -
                  rescue ArgumentError
         | 
| 456 | 
            -
                    raise unless /undefined class|marshal data too short/.match?($!.message)
         | 
| 457 | 
            -
                    raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
         | 
| 458 | 
            -
                  rescue NameError
         | 
| 459 | 
            -
                    raise unless /uninitialized constant/.match?($!.message)
         | 
| 460 | 
            -
                    raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
         | 
| 461 | 
            -
                  rescue Zlib::Error
         | 
| 462 | 
            -
                    raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
         | 
| 463 | 
            -
                  end
         | 
| 464 | 
            -
             | 
| 465 | 
            -
                  def data_cas_response
         | 
| 466 | 
            -
                    (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
         | 
| 467 | 
            -
                    data = read(count) if count > 0
         | 
| 468 | 
            -
                    if status == 1
         | 
| 469 | 
            -
                      nil
         | 
| 470 | 
            -
                    elsif status != 0
         | 
| 471 | 
            -
                      raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
         | 
| 472 | 
            -
                    elsif data
         | 
| 473 | 
            -
                      flags = data[0...extras].unpack1("N")
         | 
| 474 | 
            -
                      value = data[extras..-1]
         | 
| 475 | 
            -
                      data = deserialize(value, flags)
         | 
| 476 | 
            -
                    end
         | 
| 477 | 
            -
                    [data, cas]
         | 
| 478 | 
            -
                  end
         | 
| 479 | 
            -
             | 
| 480 | 
            -
                  CAS_HEADER = "@4CCnNNQ"
         | 
| 481 | 
            -
                  NORMAL_HEADER = "@4CCnN"
         | 
| 482 | 
            -
                  KV_HEADER = "@2n@6nN@16Q"
         | 
| 483 | 
            -
             | 
| 484 | 
            -
                  def guard_max_value(key, value)
         | 
| 485 | 
            -
                    return if value.bytesize <= @options[:value_max_bytes]
         | 
| 486 | 
            -
             | 
| 487 | 
            -
                    message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
         | 
| 488 | 
            -
                    raise Dalli::ValueOverMaxSize, message
         | 
| 489 | 
            -
                  end
         | 
| 490 | 
            -
             | 
| 491 | 
            -
                  # https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
         | 
| 492 | 
            -
                  # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
         | 
| 493 | 
            -
                  MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30 * 24 * 60 * 60 # 30 days
         | 
| 494 | 
            -
                  def sanitize_ttl(ttl)
         | 
| 495 | 
            -
                    ttl_as_i = ttl.to_i
         | 
| 496 | 
            -
                    return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
         | 
| 497 | 
            -
                    now = Time.now.to_i
         | 
| 498 | 
            -
                    return ttl_as_i if ttl_as_i > now # already a timestamp
         | 
| 499 | 
            -
                    Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
         | 
| 500 | 
            -
                    now + ttl_as_i
         | 
| 501 | 
            -
                  end
         | 
| 502 | 
            -
             | 
| 503 | 
            -
                  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
         | 
| 504 | 
            -
                  class NilObject; end
         | 
| 505 | 
            -
                  NOT_FOUND = NilObject.new
         | 
| 506 | 
            -
             | 
| 507 | 
            -
                  def generic_response(unpack = false, cache_nils = false)
         | 
| 508 | 
            -
                    (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
         | 
| 509 | 
            -
                    data = read(count) if count > 0
         | 
| 510 | 
            -
                    if status == 1
         | 
| 511 | 
            -
                      cache_nils ? NOT_FOUND : nil
         | 
| 512 | 
            -
                    elsif status == 2 || status == 5
         | 
| 513 | 
            -
                      false # Not stored, normal status for add operation
         | 
| 514 | 
            -
                    elsif status != 0
         | 
| 515 | 
            -
                      raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
         | 
| 516 | 
            -
                    elsif data
         | 
| 517 | 
            -
                      flags = data.byteslice(0, extras).unpack1("N")
         | 
| 518 | 
            -
                      value = data.byteslice(extras, data.bytesize - extras)
         | 
| 519 | 
            -
                      unpack ? deserialize(value, flags) : value
         | 
| 520 | 
            -
                    else
         | 
| 521 | 
            -
                      true
         | 
| 522 | 
            -
                    end
         | 
| 523 | 
            -
                  end
         | 
| 524 | 
            -
             | 
| 525 | 
            -
                  def cas_response
         | 
| 526 | 
            -
                    (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
         | 
| 527 | 
            -
                    read(count) if count > 0 # this is potential data that we don't care about
         | 
| 528 | 
            -
                    if status == 1
         | 
| 529 | 
            -
                      nil
         | 
| 530 | 
            -
                    elsif status == 2 || status == 5
         | 
| 531 | 
            -
                      false # Not stored, normal status for add operation
         | 
| 532 | 
            -
                    elsif status != 0
         | 
| 533 | 
            -
                      raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
         | 
| 534 | 
            -
                    else
         | 
| 535 | 
            -
                      cas
         | 
| 536 | 
            -
                    end
         | 
| 537 | 
            -
                  end
         | 
| 538 | 
            -
             | 
| 539 | 
            -
                  def keyvalue_response
         | 
| 540 | 
            -
                    hash = {}
         | 
| 541 | 
            -
                    loop do
         | 
| 542 | 
            -
                      (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
         | 
| 543 | 
            -
                      return hash if key_length == 0
         | 
| 544 | 
            -
                      key = read(key_length)
         | 
| 545 | 
            -
                      value = read(body_length - key_length) if body_length - key_length > 0
         | 
| 546 | 
            -
                      hash[key] = value
         | 
| 547 | 
            -
                    end
         | 
| 548 | 
            -
                  end
         | 
| 549 | 
            -
             | 
| 550 | 
            -
                  def multi_response
         | 
| 551 | 
            -
                    hash = {}
         | 
| 552 | 
            -
                    loop do
         | 
| 553 | 
            -
                      (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
         | 
| 554 | 
            -
                      return hash if key_length == 0
         | 
| 555 | 
            -
                      flags = read(4).unpack1("N")
         | 
| 556 | 
            -
                      key = read(key_length)
         | 
| 557 | 
            -
                      value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
         | 
| 558 | 
            -
                      hash[key] = deserialize(value, flags)
         | 
| 559 | 
            -
                    end
         | 
| 560 | 
            -
                  end
         | 
| 561 | 
            -
             | 
| 562 | 
            -
                  def write(bytes)
         | 
| 563 | 
            -
                    @inprogress = true
         | 
| 564 | 
            -
                    result = @sock.write(bytes)
         | 
| 565 | 
            -
                    @inprogress = false
         | 
| 566 | 
            -
                    result
         | 
| 567 | 
            -
                  rescue SystemCallError, Timeout::Error => e
         | 
| 568 | 
            -
                    failure!(e)
         | 
| 569 | 
            -
                  end
         | 
| 570 | 
            -
             | 
| 571 | 
            -
                  def read(count)
         | 
| 572 | 
            -
                    @inprogress = true
         | 
| 573 | 
            -
                    data = @sock.readfull(count)
         | 
| 574 | 
            -
                    @inprogress = false
         | 
| 575 | 
            -
                    data
         | 
| 576 | 
            -
                  rescue SystemCallError, Timeout::Error, EOFError => e
         | 
| 577 | 
            -
                    failure!(e)
         | 
| 578 | 
            -
                  end
         | 
| 579 | 
            -
             | 
| 580 | 
            -
                  def read_header
         | 
| 581 | 
            -
                    read(24) || raise(Dalli::NetworkError, "No response")
         | 
| 501 | 
            +
                    @response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
         | 
| 582 502 | 
             
                  end
         | 
| 583 503 |  | 
| 584 504 | 
             
                  def connect
         | 
| @@ -586,13 +506,9 @@ module Dalli | |
| 586 506 |  | 
| 587 507 | 
             
                    begin
         | 
| 588 508 | 
             
                      @pid = Process.pid
         | 
| 589 | 
            -
                      @sock =  | 
| 590 | 
            -
             | 
| 591 | 
            -
                       | 
| 592 | 
            -
                        Dalli::Socket::TCP.open(hostname, port, self, options)
         | 
| 593 | 
            -
                      end
         | 
| 594 | 
            -
                      sasl_authentication if need_auth?
         | 
| 595 | 
            -
                      @version = version # trigger actual connect
         | 
| 509 | 
            +
                      @sock = memcached_socket
         | 
| 510 | 
            +
                      authenticate_connection if require_auth?
         | 
| 511 | 
            +
                      @version = version # Connect socket if not authed
         | 
| 596 512 | 
             
                      up!
         | 
| 597 513 | 
             
                    rescue Dalli::DalliError # SASL auth failure
         | 
| 598 514 | 
             
                      raise
         | 
| @@ -602,152 +518,27 @@ module Dalli | |
| 602 518 | 
             
                    end
         | 
| 603 519 | 
             
                  end
         | 
| 604 520 |  | 
| 605 | 
            -
                  def  | 
| 606 | 
            -
                     | 
| 607 | 
            -
             | 
| 608 | 
            -
             | 
| 609 | 
            -
             | 
| 610 | 
            -
             | 
| 611 | 
            -
             | 
| 612 | 
            -
             | 
| 613 | 
            -
                   | 
| 614 | 
            -
             | 
| 615 | 
            -
                    0 => "No error",
         | 
| 616 | 
            -
                    1 => "Key not found",
         | 
| 617 | 
            -
                    2 => "Key exists",
         | 
| 618 | 
            -
                    3 => "Value too large",
         | 
| 619 | 
            -
                    4 => "Invalid arguments",
         | 
| 620 | 
            -
                    5 => "Item not stored",
         | 
| 621 | 
            -
                    6 => "Incr/decr on a non-numeric value",
         | 
| 622 | 
            -
                    7 => "The vbucket belongs to another server",
         | 
| 623 | 
            -
                    8 => "Authentication error",
         | 
| 624 | 
            -
                    9 => "Authentication continue",
         | 
| 625 | 
            -
                    0x20 => "Authentication required",
         | 
| 626 | 
            -
                    0x81 => "Unknown command",
         | 
| 627 | 
            -
                    0x82 => "Out of memory",
         | 
| 628 | 
            -
                    0x83 => "Not supported",
         | 
| 629 | 
            -
                    0x84 => "Internal error",
         | 
| 630 | 
            -
                    0x85 => "Busy",
         | 
| 631 | 
            -
                    0x86 => "Temporary failure"
         | 
| 632 | 
            -
                  }
         | 
| 633 | 
            -
             | 
| 634 | 
            -
                  OPCODES = {
         | 
| 635 | 
            -
                    get: 0x00,
         | 
| 636 | 
            -
                    set: 0x01,
         | 
| 637 | 
            -
                    add: 0x02,
         | 
| 638 | 
            -
                    replace: 0x03,
         | 
| 639 | 
            -
                    delete: 0x04,
         | 
| 640 | 
            -
                    incr: 0x05,
         | 
| 641 | 
            -
                    decr: 0x06,
         | 
| 642 | 
            -
                    flush: 0x08,
         | 
| 643 | 
            -
                    noop: 0x0A,
         | 
| 644 | 
            -
                    version: 0x0B,
         | 
| 645 | 
            -
                    getkq: 0x0D,
         | 
| 646 | 
            -
                    append: 0x0E,
         | 
| 647 | 
            -
                    prepend: 0x0F,
         | 
| 648 | 
            -
                    stat: 0x10,
         | 
| 649 | 
            -
                    setq: 0x11,
         | 
| 650 | 
            -
                    addq: 0x12,
         | 
| 651 | 
            -
                    replaceq: 0x13,
         | 
| 652 | 
            -
                    deleteq: 0x14,
         | 
| 653 | 
            -
                    incrq: 0x15,
         | 
| 654 | 
            -
                    decrq: 0x16,
         | 
| 655 | 
            -
                    auth_negotiation: 0x20,
         | 
| 656 | 
            -
                    auth_request: 0x21,
         | 
| 657 | 
            -
                    auth_continue: 0x22,
         | 
| 658 | 
            -
                    touch: 0x1C,
         | 
| 659 | 
            -
                    gat: 0x1D
         | 
| 660 | 
            -
                  }
         | 
| 661 | 
            -
             | 
| 662 | 
            -
                  HEADER = "CCnCCnNNQ"
         | 
| 663 | 
            -
                  OP_FORMAT = {
         | 
| 664 | 
            -
                    get: "a*",
         | 
| 665 | 
            -
                    set: "NNa*a*",
         | 
| 666 | 
            -
                    add: "NNa*a*",
         | 
| 667 | 
            -
                    replace: "NNa*a*",
         | 
| 668 | 
            -
                    delete: "a*",
         | 
| 669 | 
            -
                    incr: "NNNNNa*",
         | 
| 670 | 
            -
                    decr: "NNNNNa*",
         | 
| 671 | 
            -
                    flush: "N",
         | 
| 672 | 
            -
                    noop: "",
         | 
| 673 | 
            -
                    getkq: "a*",
         | 
| 674 | 
            -
                    version: "",
         | 
| 675 | 
            -
                    stat: "a*",
         | 
| 676 | 
            -
                    append: "a*a*",
         | 
| 677 | 
            -
                    prepend: "a*a*",
         | 
| 678 | 
            -
                    auth_request: "a*a*",
         | 
| 679 | 
            -
                    auth_continue: "a*a*",
         | 
| 680 | 
            -
                    touch: "Na*",
         | 
| 681 | 
            -
                    gat: "Na*"
         | 
| 682 | 
            -
                  }
         | 
| 683 | 
            -
                  FORMAT = OP_FORMAT.each_with_object({}) { |(k, v), memo| memo[k] = HEADER + v; }
         | 
| 684 | 
            -
             | 
| 685 | 
            -
                  #######
         | 
| 686 | 
            -
                  # SASL authentication support for NorthScale
         | 
| 687 | 
            -
                  #######
         | 
| 688 | 
            -
             | 
| 689 | 
            -
                  def need_auth?
         | 
| 690 | 
            -
                    @options[:username] || ENV["MEMCACHE_USERNAME"]
         | 
| 521 | 
            +
                  def memcached_socket
         | 
| 522 | 
            +
                    if socket_type == :unix
         | 
| 523 | 
            +
                      Dalli::Socket::UNIX.open(hostname, self, options)
         | 
| 524 | 
            +
                    else
         | 
| 525 | 
            +
                      Dalli::Socket::TCP.open(hostname, port, self, options)
         | 
| 526 | 
            +
                    end
         | 
| 527 | 
            +
                  end
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                  def require_auth?
         | 
| 530 | 
            +
                    !username.nil?
         | 
| 691 531 | 
             
                  end
         | 
| 692 532 |  | 
| 693 533 | 
             
                  def username
         | 
| 694 | 
            -
                    @options[:username] || ENV[ | 
| 534 | 
            +
                    @options[:username] || ENV['MEMCACHE_USERNAME']
         | 
| 695 535 | 
             
                  end
         | 
| 696 536 |  | 
| 697 537 | 
             
                  def password
         | 
| 698 | 
            -
                    @options[:password] || ENV[ | 
| 538 | 
            +
                    @options[:password] || ENV['MEMCACHE_PASSWORD']
         | 
| 699 539 | 
             
                  end
         | 
| 700 540 |  | 
| 701 | 
            -
                   | 
| 702 | 
            -
                    Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
         | 
| 703 | 
            -
             | 
| 704 | 
            -
                    # negotiate
         | 
| 705 | 
            -
                    req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
         | 
| 706 | 
            -
                    write(req)
         | 
| 707 | 
            -
             | 
| 708 | 
            -
                    (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
         | 
| 709 | 
            -
                    raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
         | 
| 710 | 
            -
                    content = read(count).tr("\u0000", " ")
         | 
| 711 | 
            -
                    return Dalli.logger.debug("Authentication not required/supported by server") if status == 0x81
         | 
| 712 | 
            -
                    mechanisms = content.split(" ")
         | 
| 713 | 
            -
                    raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" unless mechanisms.include?("PLAIN")
         | 
| 714 | 
            -
             | 
| 715 | 
            -
                    # request
         | 
| 716 | 
            -
                    mechanism = "PLAIN"
         | 
| 717 | 
            -
                    msg = "\x0#{username}\x0#{password}"
         | 
| 718 | 
            -
                    req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
         | 
| 719 | 
            -
                    write(req)
         | 
| 720 | 
            -
             | 
| 721 | 
            -
                    (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
         | 
| 722 | 
            -
                    raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
         | 
| 723 | 
            -
                    content = read(count)
         | 
| 724 | 
            -
                    return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
         | 
| 725 | 
            -
             | 
| 726 | 
            -
                    raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
         | 
| 727 | 
            -
                    raise NotImplementedError, "No two-step authentication mechanisms supported"
         | 
| 728 | 
            -
                    # (step, msg) = sasl.receive('challenge', content)
         | 
| 729 | 
            -
                    # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
         | 
| 730 | 
            -
                  end
         | 
| 731 | 
            -
             | 
| 732 | 
            -
                  def parse_hostname(str)
         | 
| 733 | 
            -
                    res = str.match(/\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/)
         | 
| 734 | 
            -
                    raise Dalli::DalliError, "Could not parse hostname #{str}" if res.nil? || res[1] == "[]"
         | 
| 735 | 
            -
                    hostnam = res[2] || res[1]
         | 
| 736 | 
            -
                    if hostnam.start_with?("/")
         | 
| 737 | 
            -
                      socket_type = :unix
         | 
| 738 | 
            -
                      # in case of unix socket, allow only setting of weight, not port
         | 
| 739 | 
            -
                      raise Dalli::DalliError, "Could not parse hostname #{str}" if res[4]
         | 
| 740 | 
            -
                      weigh = res[3]
         | 
| 741 | 
            -
                    else
         | 
| 742 | 
            -
                      socket_type = :tcp
         | 
| 743 | 
            -
                      por = res[3] || DEFAULT_PORT
         | 
| 744 | 
            -
                      por = Integer(por)
         | 
| 745 | 
            -
                      weigh = res[4]
         | 
| 746 | 
            -
                    end
         | 
| 747 | 
            -
                    weigh ||= DEFAULT_WEIGHT
         | 
| 748 | 
            -
                    weigh = Integer(weigh)
         | 
| 749 | 
            -
                    [hostnam, por, weigh, socket_type]
         | 
| 750 | 
            -
                  end
         | 
| 541 | 
            +
                  include SaslAuthentication
         | 
| 751 542 | 
             
                end
         | 
| 752 543 | 
             
              end
         | 
| 753 544 | 
             
            end
         |