dalli 3.0.4 → 3.1.1

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,703 +1,393 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
- require "timeout"
3
+ require 'forwardable'
4
+ require 'socket'
5
+ require 'timeout'
6
+
7
+ require_relative 'binary/request_formatter'
8
+ require_relative 'binary/response_header'
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
15
-
16
- DEFAULTS = {
17
- # seconds between trying to contact a remote server
18
- down_retry_delay: 30,
19
- # connect/read/write timeout for socket operations
20
- socket_timeout: 1,
21
- # times a socket operation may fail before considering the server dead
22
- socket_max_failures: 2,
23
- # amount of time to sleep between retries when a failure occurs
24
- 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
- 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
- }
36
-
37
- 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
42
- @options = DEFAULTS.merge(options)
43
- @value_compressor = ValueCompressor.new(@options)
44
- @sock = nil
45
- @msg = nil
46
- @error = nil
47
- @pid = nil
48
- @inprogress = nil
49
- end
50
-
51
- def name
52
- if socket_type == :unix
53
- hostname
54
- else
55
- "#{hostname}:#{port}"
56
- end
20
+ extend Forwardable
21
+
22
+ attr_accessor :weight, :options
23
+
24
+ def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
25
+ def_delegators :@connection_manager, :name, :sock, :hostname, :port, :close, :connected?, :socket_timeout,
26
+ :socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
27
+
28
+ def initialize(attribs, client_options = {})
29
+ hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
30
+ @options = client_options.merge(user_creds)
31
+ @value_marshaller = ValueMarshaller.new(@options)
32
+ @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
33
+ @response_processor = ResponseProcessor.new(@connection_manager, @value_marshaller)
57
34
  end
58
35
 
59
- # Chokepoint method for instrumentation
60
- def request(op, *args)
61
- 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?
36
+ # Chokepoint method for error handling and ensuring liveness
37
+ def request(opkey, *args)
38
+ verify_state(opkey)
39
+
63
40
  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."
41
+ send(opkey, *args)
42
+ rescue Dalli::MarshalError => e
43
+ log_marshal_err(args.first, e)
68
44
  raise
69
- rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
45
+ rescue Dalli::DalliError
70
46
  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")
47
+ rescue StandardError => e
48
+ log_unexpected_err(e)
74
49
  down!
75
50
  end
76
51
  end
77
52
 
53
+ ##
54
+ # Boolean method used by clients of this class to determine if this
55
+ # particular memcached instance is available for use.
78
56
  def alive?
79
- 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
86
-
87
- connect
88
- !!@sock
57
+ ensure_connected!
89
58
  rescue Dalli::NetworkError
59
+ # ensure_connected! raises a NetworkError if connection fails. We
60
+ # want to capture that error and convert it to a boolean value here.
90
61
  false
91
62
  end
92
63
 
93
- def close
94
- return unless @sock
95
- begin
96
- @sock.close
97
- rescue
98
- nil
99
- end
100
- @sock = nil
101
- @pid = nil
102
- @inprogress = false
103
- end
104
-
105
- def lock!
106
- end
107
-
108
- def unlock!
109
- end
64
+ def lock!; end
110
65
 
111
- def serializer
112
- @options[:serializer]
113
- end
66
+ def unlock!; end
114
67
 
115
68
  # Start reading key/value pairs from this connection. This is usually called
116
69
  # after a series of GETKQ commands. A NOOP is sent, and the server begins
117
70
  # flushing responses for kv pairs that were found.
118
71
  #
119
72
  # Returns nothing.
120
- def multi_response_start
121
- verify_state
73
+ def pipeline_response_setup
74
+ verify_state(:getkq)
122
75
  write_noop
123
- @multi_buffer = +""
124
- @position = 0
125
- @inprogress = true
126
- end
127
-
128
- # Did the last call to #multi_response_start complete successfully?
129
- def multi_response_completed?
130
- @multi_buffer.nil?
76
+ response_buffer.reset
77
+ @connection_manager.start_request!
131
78
  end
132
79
 
133
80
  # Attempt to receive and parse as many key/value pairs as possible
134
- # from this server. After #multi_response_start, this should be invoked
81
+ # from this server. After #pipeline_response_setup, this should be invoked
135
82
  # repeatedly whenever this server's socket is readable until
136
- # #multi_response_completed?.
83
+ # #pipeline_complete?.
137
84
  #
138
85
  # Returns a Hash of kv pairs received.
139
- def multi_response_nonblock
140
- reconnect! "multi_response has completed" if @multi_buffer.nil?
141
-
142
- @multi_buffer << @sock.read_available
143
- buf = @multi_buffer
144
- pos = @position
86
+ def pipeline_next_responses
87
+ reconnect_on_pipeline_complete!
145
88
  values = {}
146
89
 
147
- while buf.bytesize - pos >= 24
148
- header = buf.slice(pos, 24)
149
- (key_length, _, body_length, cas) = header.unpack(KV_HEADER)
150
-
151
- if key_length == 0
152
- # all done!
153
- @multi_buffer = nil
154
- @position = nil
155
- @inprogress = false
156
- break
157
-
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
169
-
170
- else
171
- # not enough data yet, wait for more
172
- break
173
- end
90
+ response_buffer.read
91
+
92
+ resp_header, key, value = pipeline_response
93
+ # resp_header is not nil only if we have a full response to parse
94
+ # in the buffer
95
+ while resp_header
96
+ # If the status is ok and key is nil, then this is the response
97
+ # to the noop at the end of the pipeline
98
+ finish_pipeline && break if resp_header.ok? && key.nil?
99
+
100
+ # If the status is ok and the key is not nil, then this is a
101
+ # getkq response with a value that we want to set in the response hash
102
+ values[key] = [value, resp_header.cas] unless key.nil?
103
+
104
+ # Get the next response from the buffer
105
+ resp_header, key, value = pipeline_response
174
106
  end
175
- @position = pos
176
107
 
177
108
  values
178
109
  rescue SystemCallError, Timeout::Error, EOFError => e
179
- failure!(e)
110
+ @connection_manager.error_on_request!(e)
180
111
  end
181
112
 
182
- # Abort an earlier #multi_response_start. Used to signal an external
183
- # timeout. The underlying socket is disconnected, and the exception is
184
- # swallowed.
113
+ # Abort current pipelined get. Generally used to signal an external
114
+ # timeout during pipelined get. The underlying socket is
115
+ # disconnected, and the exception is swallowed.
185
116
  #
186
117
  # Returns nothing.
187
- def multi_response_abort
188
- @multi_buffer = nil
189
- @position = nil
190
- @inprogress = false
191
- failure!(RuntimeError.new("External timeout"))
118
+ def pipeline_abort
119
+ response_buffer.clear
120
+ @connection_manager.abort_request!
121
+ return true unless connected?
122
+
123
+ # Closes the connection, which ensures that our connection
124
+ # is in a clean state for future requests
125
+ @connection_manager.error_on_request!('External timeout')
192
126
  rescue NetworkError
193
127
  true
194
128
  end
195
129
 
196
- # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
130
+ # Did the last call to #pipeline_response_setup complete successfully?
131
+ def pipeline_complete?
132
+ !response_buffer.in_progress?
133
+ end
197
134
 
198
- private
135
+ def username
136
+ @options[:username] || ENV['MEMCACHE_USERNAME']
137
+ end
199
138
 
200
- 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
139
+ def password
140
+ @options[:password] || ENV['MEMCACHE_PASSWORD']
207
141
  end
208
142
 
209
- def reconnect!(message)
210
- close
211
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
212
- raise Dalli::NetworkError, message
143
+ def require_auth?
144
+ !username.nil?
213
145
  end
214
146
 
215
- def failure!(exception)
216
- message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
217
- Dalli.logger.warn { message }
147
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
218
148
 
219
- @fail_count += 1
220
- if @fail_count >= options[:socket_max_failures]
221
- down!
222
- else
223
- reconnect! "Socket operation failed, retrying..."
224
- end
225
- end
149
+ private
226
150
 
227
- def down!
228
- close
151
+ ##
152
+ # Checks to see if we can execute the specified operation. Checks
153
+ # whether the connection is in use, and whether the command is allowed
154
+ ##
155
+ def verify_state(opkey)
156
+ @connection_manager.confirm_ready!
157
+ verify_allowed_quiet!(opkey) if quiet?
229
158
 
230
- @last_down_at = Time.now
159
+ # The ensure_connected call has the side effect of connecting the
160
+ # underlying socket if it is not connected, or there's been a disconnect
161
+ # because of timeout or other error. Method raises an error
162
+ # if it can't connect
163
+ raise_down_error unless ensure_connected!
164
+ end
231
165
 
232
- if @down_at
233
- time = Time.now - @down_at
234
- Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
235
- else
236
- @down_at = @last_down_at
237
- Dalli.logger.warn { "#{name} is down" }
238
- end
166
+ # The socket connection to the underlying server is initialized as a side
167
+ # effect of this call. In fact, this is the ONLY place where that
168
+ # socket connection is initialized.
169
+ #
170
+ # Both this method and connect need to be in this class so we can do auth
171
+ # as required
172
+ #
173
+ # Since this is invoked exclusively in verify_state!, we don't need to worry about
174
+ # thread safety. Using it elsewhere may require revisiting that assumption.
175
+ def ensure_connected!
176
+ return true if connected?
177
+ return false unless reconnect_down_server?
239
178
 
240
- @error = $!&.class&.name
241
- @msg ||= $!&.message
242
- raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
179
+ connect # This call needs to be in this class so we can do auth
180
+ connected?
243
181
  end
244
182
 
245
- 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
183
+ ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
184
+ def verify_allowed_quiet!(opkey)
185
+ return if ALLOWED_QUIET_OPS.include?(opkey)
250
186
 
251
- @fail_count = 0
252
- @down_at = nil
253
- @last_down_at = nil
254
- @msg = nil
255
- @error = nil
187
+ raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
256
188
  end
257
189
 
258
- def multi?
259
- Thread.current[:dalli_multi]
190
+ def quiet?
191
+ Thread.current[::Dalli::QUIET]
260
192
  end
261
193
 
194
+ def cache_nils?(opts)
195
+ return false unless opts.is_a?(Hash)
196
+
197
+ opts[:cache_nils] ? true : false
198
+ end
199
+
200
+ # Retrieval Commands
262
201
  def get(key, options = nil)
263
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
202
+ req = RequestFormatter.standard_request(opkey: :get, key: key)
264
203
  write(req)
265
- generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
204
+ @response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
266
205
  end
267
206
 
268
- def send_multiget(keys)
269
- req = +""
270
- keys.each do |key|
271
- req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
272
- end
273
- # Could send noop here instead of in multi_response_start
207
+ def gat(key, ttl, options = nil)
208
+ ttl = TtlSanitizer.sanitize(ttl)
209
+ req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
274
210
  write(req)
211
+ @response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
275
212
  end
276
213
 
277
- def set(key, value, ttl, cas, options)
278
- (value, flags) = serialize(key, value, options)
214
+ def touch(key, ttl)
279
215
  ttl = TtlSanitizer.sanitize(ttl)
216
+ write(RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl))
217
+ @response_processor.generic_response
218
+ end
280
219
 
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])
220
+ # TODO: This is confusing, as there's a cas command in memcached
221
+ # and this isn't it. Maybe rename? Maybe eliminate?
222
+ def cas(key)
223
+ req = RequestFormatter.standard_request(opkey: :get, key: key)
284
224
  write(req)
285
- cas_response unless multi?
225
+ @response_processor.data_cas_response
286
226
  end
287
227
 
288
- 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)
228
+ # Storage Commands
229
+ def set(key, value, ttl, cas, options)
230
+ opkey = quiet? ? :setq : :set
231
+ storage_req(opkey, key, value, ttl, cas, options)
232
+ end
293
233
 
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?
234
+ def add(key, value, ttl, options)
235
+ opkey = quiet? ? :addq : :add
236
+ storage_req(opkey, key, value, ttl, 0, options)
297
237
  end
298
238
 
299
239
  def replace(key, value, ttl, cas, options)
300
- (value, flags) = serialize(key, value, options)
301
- ttl = TtlSanitizer.sanitize(ttl)
240
+ opkey = quiet? ? :replaceq : :replace
241
+ storage_req(opkey, key, value, ttl, cas, options)
242
+ end
302
243
 
303
- guard_max_value(key, value)
244
+ # rubocop:disable Metrics/ParameterLists
245
+ def storage_req(opkey, key, value, ttl, cas, options)
246
+ (value, bitflags) = @value_marshaller.store(key, value, options)
247
+ ttl = TtlSanitizer.sanitize(ttl)
304
248
 
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])
249
+ req = RequestFormatter.standard_request(opkey: opkey, key: key,
250
+ value: value, bitflags: bitflags,
251
+ ttl: ttl, cas: cas)
306
252
  write(req)
307
- cas_response unless multi?
253
+ @response_processor.storage_response unless quiet?
308
254
  end
255
+ # rubocop:enable Metrics/ParameterLists
309
256
 
310
- def delete(key, cas)
311
- req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
312
- write(req)
313
- generic_response unless multi?
257
+ def append(key, value)
258
+ opkey = quiet? ? :appendq : :append
259
+ write_append_prepend opkey, key, value
314
260
  end
315
261
 
316
- def flush(ttl)
317
- req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
318
- write(req)
319
- generic_response
262
+ def prepend(key, value)
263
+ opkey = quiet? ? :prependq : :prepend
264
+ write_append_prepend opkey, key, value
320
265
  end
321
266
 
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
267
+ def write_append_prepend(opkey, key, value)
268
+ write(RequestFormatter.standard_request(opkey: opkey, key: key, value: value))
269
+ @response_processor.no_body_response unless quiet?
331
270
  end
332
271
 
333
- def decr(key, count, ttl, default)
334
- decr_incr :decr, key, count, ttl, default
272
+ # Delete Commands
273
+ def delete(key, cas)
274
+ opkey = quiet? ? :deleteq : :delete
275
+ req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
276
+ write(req)
277
+ @response_processor.no_body_response unless quiet?
335
278
  end
336
279
 
337
- def incr(key, count, ttl, default)
338
- decr_incr :incr, key, count, ttl, default
280
+ # Arithmetic Commands
281
+ def decr(key, count, ttl, initial)
282
+ opkey = quiet? ? :decrq : :decr
283
+ decr_incr opkey, key, count, ttl, initial
339
284
  end
340
285
 
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])
286
+ def incr(key, count, ttl, initial)
287
+ opkey = quiet? ? :incrq : :incr
288
+ decr_incr opkey, key, count, ttl, initial
343
289
  end
344
290
 
345
- def write_generic(bytes)
346
- write(bytes)
347
- generic_response
291
+ # This allows us to special case a nil initial value, and
292
+ # handle it differently than a zero. This special value
293
+ # for expiry causes memcached to return a not found
294
+ # if the key doesn't already exist, rather than
295
+ # setting the initial value
296
+ NOT_FOUND_EXPIRY = 0xFFFFFFFF
297
+
298
+ def decr_incr(opkey, key, count, ttl, initial)
299
+ expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
300
+ initial ||= 0
301
+ write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
302
+ count: count, initial: initial, expiry: expiry))
303
+ @response_processor.decr_incr_response unless quiet?
348
304
  end
349
305
 
350
- def write_noop
351
- req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
352
- write(req)
306
+ # Other Commands
307
+ def flush(ttl = 0)
308
+ opkey = quiet? ? :flushq : :flush
309
+ write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
310
+ @response_processor.no_body_response unless quiet?
353
311
  end
354
312
 
355
313
  # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
356
314
  # We need to read all the responses at once.
357
315
  def noop
358
316
  write_noop
359
- multi_response
317
+ @response_processor.multi_with_keys_response
360
318
  end
361
319
 
362
- def append(key, value)
363
- write_append_prepend :append, key, value
364
- end
365
-
366
- def prepend(key, value)
367
- write_append_prepend :prepend, key, value
368
- end
369
-
370
- def stats(info = "")
371
- req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
320
+ def stats(info = '')
321
+ req = RequestFormatter.standard_request(opkey: :stat, key: info)
372
322
  write(req)
373
- keyvalue_response
323
+ @response_processor.multi_with_keys_response
374
324
  end
375
325
 
376
326
  def reset_stats
377
- write_generic [REQUEST, OPCODES[:stat], "reset".bytesize, 0, 0, 0, "reset".bytesize, 0, 0, "reset"].pack(FORMAT[:stat])
378
- end
379
-
380
- def cas(key)
381
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
382
- write(req)
383
- data_cas_response
327
+ write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
328
+ @response_processor.generic_response
384
329
  end
385
330
 
386
331
  def version
387
- write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
332
+ write(RequestFormatter.standard_request(opkey: :version))
333
+ @response_processor.generic_response
388
334
  end
389
335
 
390
- def touch(key, ttl)
391
- 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])
393
- end
394
-
395
- def gat(key, ttl, options = nil)
396
- ttl = TtlSanitizer.sanitize(ttl)
397
- req = [REQUEST, OPCODES[:gat], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:gat])
336
+ def write_noop
337
+ req = RequestFormatter.standard_request(opkey: :noop)
398
338
  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
339
  end
494
340
 
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
341
+ def connect
342
+ @connection_manager.establish_connection
343
+ authenticate_connection if require_auth?
344
+ @version = version # Connect socket if not authed
345
+ up!
346
+ rescue Dalli::DalliError
347
+ raise
507
348
  end
508
349
 
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
350
+ def pipelined_get(keys)
351
+ req = +''
352
+ keys.each do |key|
353
+ req << RequestFormatter.standard_request(opkey: :getkq, key: key)
517
354
  end
355
+ # Could send noop here instead of in pipeline_response_setup
356
+ write(req)
518
357
  end
519
358
 
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
359
+ def response_buffer
360
+ @response_buffer ||= ResponseBuffer.new(@connection_manager, @response_processor)
530
361
  end
531
362
 
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)
363
+ def pipeline_response
364
+ response_buffer.process_single_getk_response
539
365
  end
540
366
 
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
367
+ # Called after the noop response is received at the end of a set
368
+ # of pipelined gets
369
+ def finish_pipeline
370
+ response_buffer.clear
371
+ @connection_manager.finish_request!
549
372
 
550
- def read_header
551
- read(24) || raise(Dalli::NetworkError, "No response")
373
+ true # to simplify response
552
374
  end
553
375
 
554
- def connect
555
- Dalli.logger.debug { "Dalli::Server#connect #{name}" }
556
-
557
- begin
558
- @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
566
- up!
567
- rescue Dalli::DalliError # SASL auth failure
568
- raise
569
- rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
570
- # SocketError = DNS resolution failure
571
- failure!(e)
572
- end
376
+ def reconnect_on_pipeline_complete!
377
+ @connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
573
378
  end
574
379
 
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"]
380
+ def log_marshal_err(key, err)
381
+ Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
382
+ Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
661
383
  end
662
384
 
663
- def username
664
- @options[:username] || ENV["MEMCACHE_USERNAME"]
665
- end
666
-
667
- def password
668
- @options[:password] || ENV["MEMCACHE_PASSWORD"]
385
+ def log_unexpected_err(err)
386
+ Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
387
+ Dalli.logger.error err.backtrace.join("\n\t")
669
388
  end
670
389
 
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
390
+ include SaslAuthentication
701
391
  end
702
392
  end
703
393
  end