dalli 3.0.4 → 3.0.5

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.

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