dalli 3.0.6 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Contains logic for the pipelined gets implemented by the client.
6
+ ##
7
+ class PipelinedGetter
8
+ def initialize(ring, key_manager)
9
+ @ring = ring
10
+ @key_manager = key_manager
11
+ end
12
+
13
+ ##
14
+ # Yields, one at a time, keys and their values+attributes.
15
+ #
16
+ def process(keys, &block)
17
+ return {} if keys.empty?
18
+
19
+ @ring.lock do
20
+ servers = setup_requests(keys)
21
+ start_time = Time.now
22
+ servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
23
+ end
24
+ rescue NetworkError => e
25
+ Dalli.logger.debug { e.inspect }
26
+ Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
27
+ retry
28
+ end
29
+
30
+ def setup_requests(keys)
31
+ groups = groups_for_keys(keys)
32
+ make_getkq_requests(groups)
33
+
34
+ # TODO: How does this exit on a NetworkError
35
+ finish_queries(groups.keys)
36
+ end
37
+
38
+ ##
39
+ # Loop through the server-grouped sets of keys, writing
40
+ # the corresponding getkq requests to the appropriate servers
41
+ #
42
+ # It's worth noting that we could potentially reduce bytes
43
+ # on the wire by switching from getkq to getq, and using
44
+ # the opaque value to match requests to responses.
45
+ ##
46
+ def make_getkq_requests(groups)
47
+ groups.each do |server, keys_for_server|
48
+ server.request(:pipelined_get, keys_for_server)
49
+ rescue DalliError, NetworkError => e
50
+ Dalli.logger.debug { e.inspect }
51
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
52
+ end
53
+ end
54
+
55
+ ##
56
+ # This loops through the servers that have keys in
57
+ # our set, sending the noop to terminate the set of queries.
58
+ ##
59
+ def finish_queries(servers)
60
+ deleted = []
61
+
62
+ servers.each do |server|
63
+ next unless server.alive?
64
+
65
+ begin
66
+ finish_query_for_server(server)
67
+ rescue Dalli::NetworkError
68
+ raise
69
+ rescue Dalli::DalliError
70
+ deleted.append(server)
71
+ end
72
+ end
73
+
74
+ servers.delete_if { |server| deleted.include?(server) }
75
+ rescue Dalli::NetworkError
76
+ abort_without_timeout(servers)
77
+ raise
78
+ end
79
+
80
+ def finish_query_for_server(server)
81
+ server.pipeline_response_setup
82
+ rescue Dalli::NetworkError
83
+ raise
84
+ rescue Dalli::DalliError => e
85
+ Dalli.logger.debug { e.inspect }
86
+ Dalli.logger.debug { "Results from server: #{server.name} will be missing from the results" }
87
+ raise
88
+ end
89
+
90
+ # Swallows Dalli::NetworkError
91
+ def abort_without_timeout(servers)
92
+ servers.each(&:pipeline_abort)
93
+ end
94
+
95
+ def fetch_responses(servers, start_time, timeout, &block)
96
+ # Remove any servers which are not connected
97
+ servers.delete_if { |s| !s.connected? }
98
+ return [] if servers.empty?
99
+
100
+ time_left = remaining_time(start_time, timeout)
101
+ readable_servers = servers_with_response(servers, time_left)
102
+ if readable_servers.empty?
103
+ abort_with_timeout(servers)
104
+ return []
105
+ end
106
+
107
+ # Loop through the servers with responses, and
108
+ # delete any from our list that are finished
109
+ readable_servers.each do |server|
110
+ servers.delete(server) if process_server(server, &block)
111
+ end
112
+ servers
113
+ rescue NetworkError
114
+ # Abort and raise if we encountered a network error. This triggers
115
+ # a retry at the top level.
116
+ abort_without_timeout(servers)
117
+ raise
118
+ end
119
+
120
+ def remaining_time(start, timeout)
121
+ elapsed = Time.now - start
122
+ return 0 if elapsed > timeout
123
+
124
+ timeout - elapsed
125
+ end
126
+
127
+ # Swallows Dalli::NetworkError
128
+ def abort_with_timeout(servers)
129
+ abort_without_timeout(servers)
130
+ servers.each do |server|
131
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
132
+ end
133
+
134
+ true # Required to simplify caller
135
+ end
136
+
137
+ # Processes responses from a server. Returns true if there are no
138
+ # additional responses from this server.
139
+ def process_server(server)
140
+ server.pipeline_next_responses.each_pair do |key, value_list|
141
+ yield @key_manager.key_without_namespace(key), value_list
142
+ end
143
+
144
+ server.pipeline_complete?
145
+ end
146
+
147
+ def servers_with_response(servers, timeout)
148
+ return [] if servers.empty?
149
+
150
+ # TODO: - This is a bit challenging. Essentially the PipelinedGetter
151
+ # is a reactor, but without the benefit of a Fiber or separate thread.
152
+ # My suspicion is that we may want to try and push this down into the
153
+ # individual servers, but I'm not sure. For now, we keep the
154
+ # mapping between the alerted object (the socket) and the
155
+ # corrresponding server here.
156
+ server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
157
+
158
+ readable, = IO.select(server_map.keys, nil, nil, timeout)
159
+ return [] if readable.nil?
160
+
161
+ readable.map { |sock| server_map[sock] }
162
+ end
163
+
164
+ def groups_for_keys(*keys)
165
+ keys.flatten!
166
+ keys.map! { |a| @key_manager.validate_key(a.to_s) }
167
+ groups = @ring.keys_grouped_by_server(keys)
168
+ if (unfound_keys = groups.delete(nil))
169
+ Dalli.logger.debug do
170
+ "unable to get keys for #{unfound_keys.length} keys "\
171
+ 'because no matching server was found'
172
+ end
173
+ end
174
+ groups
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'socket'
5
+ require 'timeout'
6
+
7
+ module Dalli
8
+ module Protocol
9
+ ##
10
+ # Base class for a single Memcached server, containing logic common to all
11
+ # protocols. Contains logic for managing connection state to the server and value
12
+ # handling.
13
+ ##
14
+ class Base
15
+ extend Forwardable
16
+
17
+ attr_accessor :weight, :options
18
+
19
+ def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
20
+ def_delegators :@connection_manager, :name, :sock, :hostname, :port, :close, :connected?, :socket_timeout,
21
+ :socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
22
+
23
+ def initialize(attribs, client_options = {})
24
+ hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
25
+ @options = client_options.merge(user_creds)
26
+ @value_marshaller = ValueMarshaller.new(@options)
27
+ @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
28
+ end
29
+
30
+ # Chokepoint method for error handling and ensuring liveness
31
+ def request(opkey, *args)
32
+ verify_state(opkey)
33
+
34
+ begin
35
+ send(opkey, *args)
36
+ rescue Dalli::MarshalError => e
37
+ log_marshal_err(args.first, e)
38
+ raise
39
+ rescue Dalli::DalliError
40
+ raise
41
+ rescue StandardError => e
42
+ log_unexpected_err(e)
43
+ down!
44
+ end
45
+ end
46
+
47
+ ##
48
+ # Boolean method used by clients of this class to determine if this
49
+ # particular memcached instance is available for use.
50
+ def alive?
51
+ ensure_connected!
52
+ rescue Dalli::NetworkError
53
+ # ensure_connected! raises a NetworkError if connection fails. We
54
+ # want to capture that error and convert it to a boolean value here.
55
+ false
56
+ end
57
+
58
+ def lock!; end
59
+
60
+ def unlock!; end
61
+
62
+ # Start reading key/value pairs from this connection. This is usually called
63
+ # after a series of GETKQ commands. A NOOP is sent, and the server begins
64
+ # flushing responses for kv pairs that were found.
65
+ #
66
+ # Returns nothing.
67
+ def pipeline_response_setup
68
+ verify_state(:getkq)
69
+ write_noop
70
+ response_buffer.reset
71
+ @connection_manager.start_request!
72
+ end
73
+
74
+ # Attempt to receive and parse as many key/value pairs as possible
75
+ # from this server. After #pipeline_response_setup, this should be invoked
76
+ # repeatedly whenever this server's socket is readable until
77
+ # #pipeline_complete?.
78
+ #
79
+ # Returns a Hash of kv pairs received.
80
+ def pipeline_next_responses
81
+ reconnect_on_pipeline_complete!
82
+ values = {}
83
+
84
+ response_buffer.read
85
+
86
+ resp_header, key, value = pipeline_response
87
+ # resp_header is not nil only if we have a full response to parse
88
+ # in the buffer
89
+ while resp_header
90
+ # If the status is ok and key is nil, then this is the response
91
+ # to the noop at the end of the pipeline
92
+ finish_pipeline && break if resp_header.ok? && key.nil?
93
+
94
+ # If the status is ok and the key is not nil, then this is a
95
+ # getkq response with a value that we want to set in the response hash
96
+ values[key] = [value, resp_header.cas] unless key.nil?
97
+
98
+ # Get the next response from the buffer
99
+ resp_header, key, value = pipeline_response
100
+ end
101
+
102
+ values
103
+ rescue SystemCallError, Timeout::Error, EOFError => e
104
+ @connection_manager.error_on_request!(e)
105
+ end
106
+
107
+ # Abort current pipelined get. Generally used to signal an external
108
+ # timeout during pipelined get. The underlying socket is
109
+ # disconnected, and the exception is swallowed.
110
+ #
111
+ # Returns nothing.
112
+ def pipeline_abort
113
+ response_buffer.clear
114
+ @connection_manager.abort_request!
115
+ return true unless connected?
116
+
117
+ # Closes the connection, which ensures that our connection
118
+ # is in a clean state for future requests
119
+ @connection_manager.error_on_request!('External timeout')
120
+ rescue NetworkError
121
+ true
122
+ end
123
+
124
+ # Did the last call to #pipeline_response_setup complete successfully?
125
+ def pipeline_complete?
126
+ !response_buffer.in_progress?
127
+ end
128
+
129
+ def username
130
+ @options[:username] || ENV['MEMCACHE_USERNAME']
131
+ end
132
+
133
+ def password
134
+ @options[:password] || ENV['MEMCACHE_PASSWORD']
135
+ end
136
+
137
+ def require_auth?
138
+ !username.nil?
139
+ end
140
+
141
+ def quiet?
142
+ Thread.current[::Dalli::QUIET]
143
+ end
144
+ alias multi? quiet?
145
+
146
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
147
+
148
+ private
149
+
150
+ ##
151
+ # Checks to see if we can execute the specified operation. Checks
152
+ # whether the connection is in use, and whether the command is allowed
153
+ ##
154
+ def verify_state(opkey)
155
+ @connection_manager.confirm_ready!
156
+ verify_allowed_quiet!(opkey) if quiet?
157
+
158
+ # The ensure_connected call has the side effect of connecting the
159
+ # underlying socket if it is not connected, or there's been a disconnect
160
+ # because of timeout or other error. Method raises an error
161
+ # if it can't connect
162
+ raise_down_error unless ensure_connected!
163
+ end
164
+
165
+ # The socket connection to the underlying server is initialized as a side
166
+ # effect of this call. In fact, this is the ONLY place where that
167
+ # socket connection is initialized.
168
+ #
169
+ # Both this method and connect need to be in this class so we can do auth
170
+ # as required
171
+ #
172
+ # Since this is invoked exclusively in verify_state!, we don't need to worry about
173
+ # thread safety. Using it elsewhere may require revisiting that assumption.
174
+ def ensure_connected!
175
+ return true if connected?
176
+ return false unless reconnect_down_server?
177
+
178
+ connect # This call needs to be in this class so we can do auth
179
+ connected?
180
+ end
181
+
182
+ def cache_nils?(opts)
183
+ return false unless opts.is_a?(Hash)
184
+
185
+ opts[:cache_nils] ? true : false
186
+ end
187
+
188
+ def connect
189
+ @connection_manager.establish_connection
190
+ authenticate_connection if require_auth?
191
+ @version = version # Connect socket if not authed
192
+ up!
193
+ rescue Dalli::DalliError
194
+ raise
195
+ end
196
+
197
+ def pipelined_get(keys)
198
+ req = +''
199
+ keys.each do |key|
200
+ req << quiet_get_request(key)
201
+ end
202
+ # Could send noop here instead of in pipeline_response_setup
203
+ write(req)
204
+ end
205
+
206
+ def response_buffer
207
+ @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
208
+ end
209
+
210
+ def pipeline_response
211
+ response_buffer.process_single_getk_response
212
+ end
213
+
214
+ # Called after the noop response is received at the end of a set
215
+ # of pipelined gets
216
+ def finish_pipeline
217
+ response_buffer.clear
218
+ @connection_manager.finish_request!
219
+
220
+ true # to simplify response
221
+ end
222
+
223
+ def reconnect_on_pipeline_complete!
224
+ @connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
225
+ end
226
+
227
+ def log_marshal_err(key, err)
228
+ Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
229
+ Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
230
+ end
231
+
232
+ def log_unexpected_err(err)
233
+ Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
234
+ Dalli.logger.error err.backtrace.join("\n\t")
235
+ end
236
+ end
237
+ end
238
+ end
@@ -31,11 +31,14 @@ module Dalli
31
31
  deleteq: 0x14,
32
32
  incrq: 0x15,
33
33
  decrq: 0x16,
34
+ flushq: 0x18,
35
+ appendq: 0x19,
36
+ prependq: 0x1A,
37
+ touch: 0x1C,
38
+ gat: 0x1D,
34
39
  auth_negotiation: 0x20,
35
40
  auth_request: 0x21,
36
- auth_continue: 0x22,
37
- touch: 0x1C,
38
- gat: 0x1D
41
+ auth_continue: 0x22
39
42
  }.freeze
40
43
 
41
44
  REQ_HEADER_FORMAT = 'CCnCCnNNQ'
@@ -56,6 +59,8 @@ module Dalli
56
59
 
57
60
  append: KEY_AND_VALUE,
58
61
  prepend: KEY_AND_VALUE,
62
+ appendq: KEY_AND_VALUE,
63
+ prependq: KEY_AND_VALUE,
59
64
  auth_request: KEY_AND_VALUE,
60
65
  auth_continue: KEY_AND_VALUE,
61
66
 
@@ -68,8 +73,11 @@ module Dalli
68
73
 
69
74
  incr: INCR_DECR,
70
75
  decr: INCR_DECR,
76
+ incrq: INCR_DECR,
77
+ decrq: INCR_DECR,
71
78
 
72
79
  flush: TTL_ONLY,
80
+ flushq: TTL_ONLY,
73
81
 
74
82
  noop: NO_BODY,
75
83
  auth_negotiation: NO_BODY,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Binary
6
+ ##
7
+ # Class that encapsulates data parsed from a memcached response header.
8
+ ##
9
+ class ResponseHeader
10
+ SIZE = 24
11
+ FMT = '@2nCCnNNQ'
12
+
13
+ attr_reader :key_len, :extra_len, :data_type, :status, :body_len, :opaque, :cas
14
+
15
+ def initialize(buf)
16
+ raise ArgumentError, "Response buffer must be at least #{SIZE} bytes" unless buf.bytesize >= SIZE
17
+
18
+ @key_len, @extra_len, @data_type, @status, @body_len, @opaque, @cas = buf.unpack(FMT)
19
+ end
20
+
21
+ def ok?
22
+ status.zero?
23
+ end
24
+
25
+ def not_found?
26
+ status == 1
27
+ end
28
+
29
+ NOT_STORED_STATUSES = [2, 5].freeze
30
+ def not_stored?
31
+ NOT_STORED_STATUSES.include?(status)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -9,9 +9,6 @@ module Dalli
9
9
  # and parsing into local values. Handles errors on unexpected values.
10
10
  ##
11
11
  class ResponseProcessor
12
- RESP_HEADER = '@2nCCnNNQ'
13
- RESP_HEADER_SIZE = 24
14
-
15
12
  # Response codes taken from:
16
13
  # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
17
14
  RESPONSE_CODES = {
@@ -44,14 +41,9 @@ module Dalli
44
41
  end
45
42
 
46
43
  def read_response
47
- status, extra_len, key_len, body_len, cas = unpack_header(read_header)
48
- body = read(body_len) if body_len.positive?
49
- [status, extra_len, body, cas, key_len]
50
- end
51
-
52
- def unpack_header(header)
53
- (key_len, extra_len, _, status, body_len, _, cas) = header.unpack(RESP_HEADER)
54
- [status, extra_len, key_len, body_len, cas]
44
+ resp_header = ResponseHeader.new(read_header)
45
+ body = read(resp_header.body_len) if resp_header.body_len.positive?
46
+ [resp_header, body]
55
47
  end
56
48
 
57
49
  def unpack_response_body(extra_len, key_len, body, unpack)
@@ -63,45 +55,65 @@ module Dalli
63
55
  end
64
56
 
65
57
  def read_header
66
- read(RESP_HEADER_SIZE) || raise(Dalli::NetworkError, 'No response')
58
+ read(ResponseHeader::SIZE) || raise(Dalli::NetworkError, 'No response')
67
59
  end
68
60
 
69
- def not_found?(status)
70
- status == 1
61
+ def raise_on_not_ok!(resp_header)
62
+ return if resp_header.ok?
63
+
64
+ raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
71
65
  end
72
66
 
73
- NOT_STORED_STATUSES = [2, 5].freeze
74
- def not_stored?(status)
75
- NOT_STORED_STATUSES.include?(status)
67
+ def generic_response(unpack: false, cache_nils: false)
68
+ resp_header, body = read_response
69
+
70
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
71
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
72
+
73
+ raise_on_not_ok!(resp_header)
74
+ return true unless body
75
+
76
+ unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
76
77
  end
77
78
 
78
- def raise_on_not_ok_status!(status)
79
- return if status.zero?
79
+ ##
80
+ # Response for a storage operation. Returns the cas on success. False
81
+ # if the value wasn't stored. And raises an error on all other error
82
+ # codes from memcached.
83
+ ##
84
+ def storage_response
85
+ resp_header, = read_response
86
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
80
87
 
81
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
88
+ raise_on_not_ok!(resp_header)
89
+ resp_header.cas
82
90
  end
83
91
 
84
- def generic_response(unpack: false, cache_nils: false)
85
- status, extra_len, body, _, key_len = read_response
92
+ def delete_response
93
+ resp_header, = read_response
94
+ return false if resp_header.not_found? || resp_header.not_stored?
86
95
 
87
- return cache_nils ? ::Dalli::NOT_FOUND : nil if not_found?(status)
88
- return false if not_stored?(status) # Not stored, normal status for add operation
96
+ raise_on_not_ok!(resp_header)
97
+ true
98
+ end
89
99
 
90
- raise_on_not_ok_status!(status)
91
- return true unless body
100
+ def no_body_response
101
+ resp_header, = read_response
102
+ return false if resp_header.not_stored? # Not stored, possible status for append/prepend
92
103
 
93
- unpack_response_body(extra_len, key_len, body, unpack).last
104
+ raise_on_not_ok!(resp_header)
105
+ true
94
106
  end
95
107
 
96
- def data_cas_response
97
- status, extra_len, body, cas, key_len = read_response
98
- return [nil, cas] if not_found?(status)
99
- return [nil, false] if not_stored?(status)
108
+ def data_cas_response(unpack: true)
109
+ resp_header, body = read_response
110
+ return [nil, resp_header.cas] if resp_header.not_found?
111
+ return [nil, false] if resp_header.not_stored?
100
112
 
101
- raise_on_not_ok_status!(status)
102
- return [nil, cas] unless body
113
+ raise_on_not_ok!(resp_header)
114
+ return [nil, resp_header.cas] unless body
103
115
 
104
- [unpack_response_body(extra_len, key_len, body, true).last, cas]
116
+ [unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last, resp_header.cas]
105
117
  end
106
118
 
107
119
  def cas_response
@@ -111,17 +123,17 @@ module Dalli
111
123
  def multi_with_keys_response
112
124
  hash = {}
113
125
  loop do
114
- status, extra_len, body, _, key_len = read_response
126
+ resp_header, body = read_response
115
127
  # This is the response to the terminating noop / end of stat
116
- return hash if status.zero? && key_len.zero?
128
+ return hash if resp_header.ok? && resp_header.key_len.zero?
117
129
 
118
130
  # Ignore any responses with non-zero status codes,
119
131
  # such as errors from set operations. That allows
120
132
  # this code to be used at the end of a multi
121
133
  # block to clear any error responses from inside the multi.
122
- next unless status.zero?
134
+ next unless resp_header.ok?
123
135
 
124
- key, value = unpack_response_body(extra_len, key_len, body, true)
136
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
125
137
  hash[key] = value
126
138
  end
127
139
  end
@@ -137,11 +149,58 @@ module Dalli
137
149
  raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
138
150
  end
139
151
 
140
- def auth_response
141
- (_, extra_len, _, status, body_len,) = read_header.unpack(RESP_HEADER)
142
- validate_auth_format(extra_len, body_len)
152
+ def auth_response(buf = read_header)
153
+ resp_header = ResponseHeader.new(buf)
154
+ body_len = resp_header.body_len
155
+ validate_auth_format(resp_header.extra_len, body_len)
143
156
  content = read(body_len) if body_len.positive?
144
- [status, content]
157
+ [resp_header.status, content]
158
+ end
159
+
160
+ def contains_header?(buf)
161
+ return false unless buf
162
+
163
+ buf.bytesize >= ResponseHeader::SIZE
164
+ end
165
+
166
+ def response_header_from_buffer(buf)
167
+ header = buf.slice(0, ResponseHeader::SIZE)
168
+ ResponseHeader.new(header)
169
+ end
170
+
171
+ ##
172
+ # This method returns an array of values used in a pipelined
173
+ # getk process. The first value is the number of bytes by
174
+ # which to advance the pointer in the buffer. If the
175
+ # complete response is found in the buffer, this will
176
+ # be the response size. Otherwise it is zero.
177
+ #
178
+ # The remaining three values in the array are the ResponseHeader,
179
+ # key, and value.
180
+ ##
181
+ def getk_response_from_buffer(buf)
182
+ # There's no header in the buffer, so don't advance
183
+ return [0, nil, nil, nil] unless contains_header?(buf)
184
+
185
+ resp_header = response_header_from_buffer(buf)
186
+ body_len = resp_header.body_len
187
+
188
+ # We have a complete response that has no body.
189
+ # This is either the response to the terminating
190
+ # noop or, if the status is not zero, an intermediate
191
+ # error response that needs to be discarded.
192
+ return [ResponseHeader::SIZE, resp_header, nil, nil] if body_len.zero?
193
+
194
+ resp_size = ResponseHeader::SIZE + body_len
195
+ # The header is in the buffer, but the body is not. As we don't have
196
+ # a complete response, don't advance the buffer
197
+ return [0, nil, nil, nil] unless buf.bytesize >= resp_size
198
+
199
+ # The full response is in our buffer, so parse it and return
200
+ # the values
201
+ body = buf.slice(ResponseHeader::SIZE, body_len)
202
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
203
+ [resp_size, resp_header, key, value]
145
204
  end
146
205
  end
147
206
  end
@@ -10,7 +10,7 @@ module Dalli
10
10
  def perform_auth_negotiation
11
11
  write(RequestFormatter.standard_request(opkey: :auth_negotiation))
12
12
 
13
- status, content = @response_processor.auth_response
13
+ status, content = response_processor.auth_response
14
14
  return [status, []] if content.nil?
15
15
 
16
16
  # Substitute spaces for the \x00 returned by