dalli 2.7.11 → 3.2.0

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.

Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -6
  3. data/History.md +124 -0
  4. data/README.md +26 -200
  5. data/lib/dalli/cas/client.rb +1 -57
  6. data/lib/dalli/client.rb +230 -263
  7. data/lib/dalli/compressor.rb +12 -2
  8. data/lib/dalli/key_manager.rb +113 -0
  9. data/lib/dalli/options.rb +6 -7
  10. data/lib/dalli/pipelined_getter.rb +177 -0
  11. data/lib/dalli/protocol/base.rb +241 -0
  12. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  13. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  14. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  15. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  16. data/lib/dalli/protocol/binary.rb +173 -0
  17. data/lib/dalli/protocol/connection_manager.rb +252 -0
  18. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  19. data/lib/dalli/protocol/meta/request_formatter.rb +108 -0
  20. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  21. data/lib/dalli/protocol/meta.rb +177 -0
  22. data/lib/dalli/protocol/response_buffer.rb +54 -0
  23. data/lib/dalli/protocol/server_config_parser.rb +84 -0
  24. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  25. data/lib/dalli/protocol/value_compressor.rb +85 -0
  26. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  27. data/lib/dalli/protocol/value_serializer.rb +91 -0
  28. data/lib/dalli/protocol.rb +8 -0
  29. data/lib/dalli/ring.rb +90 -81
  30. data/lib/dalli/server.rb +3 -749
  31. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  32. data/lib/dalli/socket.rb +117 -137
  33. data/lib/dalli/version.rb +4 -1
  34. data/lib/dalli.rb +42 -14
  35. data/lib/rack/session/dalli.rb +95 -95
  36. metadata +118 -10
  37. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
  38. data/lib/active_support/cache/dalli_store.rb +0 -441
  39. data/lib/dalli/railtie.rb +0 -8
data/lib/dalli/server.rb CHANGED
@@ -1,752 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'socket'
3
- require 'timeout'
4
2
 
5
- module Dalli
6
- class Server
7
- attr_accessor :hostname
8
- attr_accessor :port
9
- attr_accessor :weight
10
- attr_accessor :options
11
- attr_reader :sock
12
- attr_reader :socket_type # possible values: :unix, :tcp
13
-
14
- DEFAULT_PORT = 11211
15
- DEFAULT_WEIGHT = 1
16
- DEFAULTS = {
17
- # seconds between trying to contact a remote server
18
- :down_retry_delay => 60,
19
- # connect/read/write timeout for socket operations
20
- :socket_timeout => 0.5,
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.01,
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
- # surpassing value_max_bytes either warns (false) or throws (true)
28
- :error_when_over_max_size => false,
29
- :compressor => Compressor,
30
- # min byte size to attempt compression
31
- :compression_min_size => 1024,
32
- # max byte size for compression
33
- :compression_max_size => false,
34
- :serializer => Marshal,
35
- :username => nil,
36
- :password => nil,
37
- :keepalive => true,
38
- # max byte size for SO_SNDBUF
39
- :sndbuf => nil,
40
- # max byte size for SO_RCVBUF
41
- :rcvbuf => nil
42
- }
43
-
44
- def initialize(attribs, options = {})
45
- @hostname, @port, @weight, @socket_type = parse_hostname(attribs)
46
- @fail_count = 0
47
- @down_at = nil
48
- @last_down_at = nil
49
- @options = DEFAULTS.merge(options)
50
- @sock = nil
51
- @msg = nil
52
- @error = nil
53
- @pid = nil
54
- @inprogress = nil
55
- end
56
-
57
- def name
58
- if socket_type == :unix
59
- hostname
60
- else
61
- "#{hostname}:#{port}"
62
- end
63
- end
64
-
65
- # Chokepoint method for instrumentation
66
- def request(op, *args)
67
- verify_state
68
- raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
69
- begin
70
- send(op, *args)
71
- rescue Dalli::MarshalError => ex
72
- Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
73
- Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
74
- Dalli.logger.error ex.backtrace.join("\n\t")
75
- false
76
- rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
77
- raise
78
- rescue => ex
79
- Dalli.logger.error "Unexpected exception during Dalli request: #{ex.class.name}: #{ex.message}"
80
- Dalli.logger.error ex.backtrace.join("\n\t")
81
- down!
82
- end
83
- end
84
-
85
- def alive?
86
- return true if @sock
87
-
88
- if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
89
- time = @last_down_at + options[:down_retry_delay] - Time.now
90
- Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
91
- return false
92
- end
93
-
94
- connect
95
- !!@sock
96
- rescue Dalli::NetworkError
97
- false
98
- end
99
-
100
- def close
101
- return unless @sock
102
- @sock.close rescue nil
103
- @sock = nil
104
- @pid = nil
105
- @inprogress = false
106
- end
107
-
108
- def lock!
109
- end
110
-
111
- def unlock!
112
- end
113
-
114
- def serializer
115
- @options[:serializer]
116
- end
117
-
118
- def compressor
119
- @options[:compressor]
120
- end
121
-
122
- # Start reading key/value pairs from this connection. This is usually called
123
- # after a series of GETKQ commands. A NOOP is sent, and the server begins
124
- # flushing responses for kv pairs that were found.
125
- #
126
- # Returns nothing.
127
- def multi_response_start
128
- verify_state
129
- write_noop
130
- @multi_buffer = String.new('')
131
- @position = 0
132
- @inprogress = true
133
- end
134
-
135
- # Did the last call to #multi_response_start complete successfully?
136
- def multi_response_completed?
137
- @multi_buffer.nil?
138
- end
139
-
140
- # Attempt to receive and parse as many key/value pairs as possible
141
- # from this server. After #multi_response_start, this should be invoked
142
- # repeatedly whenever this server's socket is readable until
143
- # #multi_response_completed?.
144
- #
145
- # Returns a Hash of kv pairs received.
146
- def multi_response_nonblock
147
- raise 'multi_response has completed' if @multi_buffer.nil?
148
-
149
- @multi_buffer << @sock.read_available
150
- buf = @multi_buffer
151
- pos = @position
152
- values = {}
153
-
154
- while buf.bytesize - pos >= 24
155
- header = buf.slice(pos, 24)
156
- (key_length, _, body_length, cas) = header.unpack(KV_HEADER)
157
-
158
- if key_length == 0
159
- # all done!
160
- @multi_buffer = nil
161
- @position = nil
162
- @inprogress = false
163
- break
164
-
165
- elsif buf.bytesize - pos >= 24 + body_length
166
- flags = buf.slice(pos + 24, 4).unpack('N')[0]
167
- key = buf.slice(pos + 24 + 4, key_length)
168
- value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0
169
-
170
- pos = pos + 24 + body_length
171
-
172
- begin
173
- values[key] = [deserialize(value, flags), cas]
174
- rescue DalliError
175
- end
176
-
177
- else
178
- # not enough data yet, wait for more
179
- break
180
- end
181
- end
182
- @position = pos
183
-
184
- values
185
- rescue SystemCallError, Timeout::Error, EOFError => e
186
- failure!(e)
187
- end
188
-
189
- # Abort an earlier #multi_response_start. Used to signal an external
190
- # timeout. The underlying socket is disconnected, and the exception is
191
- # swallowed.
192
- #
193
- # Returns nothing.
194
- def multi_response_abort
195
- @multi_buffer = nil
196
- @position = nil
197
- @inprogress = false
198
- failure!(RuntimeError.new('External timeout'))
199
- rescue NetworkError
200
- true
201
- end
202
-
203
- # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
204
-
205
- private
206
-
207
- def verify_state
208
- failure!(RuntimeError.new('Already writing to socket')) if @inprogress
209
- if @pid && @pid != Process.pid
210
- message = 'Fork detected, re-connecting child process...'
211
- Dalli.logger.info { message }
212
- reconnect! message
213
- end
214
- end
215
-
216
- def reconnect!(message)
217
- close
218
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
219
- raise Dalli::NetworkError, message
220
- end
221
-
222
- def failure!(exception)
223
- message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
224
- Dalli.logger.warn { message }
225
-
226
- @fail_count += 1
227
- if @fail_count >= options[:socket_max_failures]
228
- down!
229
- else
230
- reconnect! 'Socket operation failed, retrying...'
231
- end
232
- end
233
-
234
- def down!
235
- close
236
-
237
- @last_down_at = Time.now
238
-
239
- if @down_at
240
- time = Time.now - @down_at
241
- Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
242
- else
243
- @down_at = @last_down_at
244
- Dalli.logger.warn { "#{name} is down" }
245
- end
246
-
247
- @error = $! && $!.class.name
248
- @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
249
- raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
250
- end
251
-
252
- def up!
253
- if @down_at
254
- time = Time.now - @down_at
255
- Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
256
- end
257
-
258
- @fail_count = 0
259
- @down_at = nil
260
- @last_down_at = nil
261
- @msg = nil
262
- @error = nil
263
- end
264
-
265
- def multi?
266
- Thread.current[:dalli_multi]
267
- end
268
-
269
- def get(key, options=nil)
270
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
271
- write(req)
272
- generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
273
- end
274
-
275
- def send_multiget(keys)
276
- req = String.new("")
277
- keys.each do |key|
278
- req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
279
- end
280
- # Could send noop here instead of in multi_response_start
281
- write(req)
282
- end
283
-
284
- def set(key, value, ttl, cas, options)
285
- (value, flags) = serialize(key, value, options)
286
- ttl = sanitize_ttl(ttl)
287
-
288
- guard_max_value(key, value) do
289
- 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])
290
- write(req)
291
- cas_response unless multi?
292
- end
293
- end
294
-
295
- def add(key, value, ttl, options)
296
- (value, flags) = serialize(key, value, options)
297
- ttl = sanitize_ttl(ttl)
298
-
299
- guard_max_value(key, value) do
300
- 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])
301
- write(req)
302
- cas_response unless multi?
303
- end
304
- end
305
-
306
- def replace(key, value, ttl, cas, options)
307
- (value, flags) = serialize(key, value, options)
308
- ttl = sanitize_ttl(ttl)
309
-
310
- guard_max_value(key, value) do
311
- 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])
312
- write(req)
313
- cas_response unless multi?
314
- end
315
- end
316
-
317
- def delete(key, cas)
318
- req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
319
- write(req)
320
- generic_response unless multi?
321
- end
322
-
323
- def flush(ttl)
324
- req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
325
- write(req)
326
- generic_response
327
- end
328
-
329
- def decr_incr(opcode, key, count, ttl, default)
330
- expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
331
- default ||= 0
332
- (h, l) = split(count)
333
- (dh, dl) = split(default)
334
- req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
335
- write(req)
336
- body = generic_response
337
- body ? body.unpack('Q>').first : body
338
- end
339
-
340
- def decr(key, count, ttl, default)
341
- decr_incr :decr, key, count, ttl, default
342
- end
343
-
344
- def incr(key, count, ttl, default)
345
- decr_incr :incr, key, count, ttl, default
346
- end
347
-
348
- def write_append_prepend(opcode, key, value)
349
- write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode])
350
- end
351
-
352
- def write_generic(bytes)
353
- write(bytes)
354
- generic_response
355
- end
356
-
357
- def write_noop
358
- req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
359
- write(req)
360
- end
361
-
362
- # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
363
- # We need to read all the responses at once.
364
- def noop
365
- write_noop
366
- multi_response
367
- end
368
-
369
- def append(key, value)
370
- write_append_prepend :append, key, value
371
- end
372
-
373
- def prepend(key, value)
374
- write_append_prepend :prepend, key, value
375
- end
376
-
377
- def stats(info='')
378
- req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
379
- write(req)
380
- keyvalue_response
381
- end
382
-
383
- def reset_stats
384
- write_generic [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
385
- end
386
-
387
- def cas(key)
388
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
389
- write(req)
390
- data_cas_response
391
- end
392
-
393
- def version
394
- write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
395
- end
396
-
397
- def touch(key, ttl)
398
- ttl = sanitize_ttl(ttl)
399
- write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
400
- end
401
-
402
- # http://www.hjp.at/zettel/m/memcached_flags.rxml
403
- # Looks like most clients use bit 0 to indicate native language serialization
404
- # and bit 1 to indicate gzip compression.
405
- FLAG_SERIALIZED = 0x1
406
- FLAG_COMPRESSED = 0x2
407
-
408
- def serialize(key, value, options=nil)
409
- marshalled = false
410
- value = unless options && options[:raw]
411
- marshalled = true
412
- begin
413
- self.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
- else
424
- value.to_s
425
- end
426
- compressed = false
427
- set_compress_option = true if options && options[:compress]
428
- if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size] &&
429
- (!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size])
430
- value = self.compressor.compress(value)
431
- compressed = true
432
- end
433
-
434
- flags = 0
435
- flags |= FLAG_COMPRESSED if compressed
436
- flags |= FLAG_SERIALIZED if marshalled
437
- [value, flags]
438
- end
439
-
440
- def deserialize(value, flags)
441
- value = self.compressor.decompress(value) if (flags & FLAG_COMPRESSED) != 0
442
- value = self.serializer.load(value) if (flags & FLAG_SERIALIZED) != 0
443
- value
444
- rescue TypeError
445
- raise if $!.message !~ /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/
446
- raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
447
- rescue ArgumentError
448
- raise if $!.message !~ /undefined class|marshal data too short/
449
- raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
450
- rescue NameError
451
- raise if $!.message !~ /uninitialized constant/
452
- raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
453
- rescue Zlib::Error
454
- raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
455
- end
456
-
457
- def data_cas_response
458
- (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
459
- data = read(count) if count > 0
460
- if status == 1
461
- nil
462
- elsif status != 0
463
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
464
- elsif data
465
- flags = data[0...extras].unpack('N')[0]
466
- value = data[extras..-1]
467
- data = deserialize(value, flags)
468
- end
469
- [data, cas]
470
- end
471
-
472
- CAS_HEADER = '@4CCnNNQ'
473
- NORMAL_HEADER = '@4CCnN'
474
- KV_HEADER = '@2n@6nN@16Q'
475
-
476
- def guard_max_value(key, value)
477
- if value.bytesize <= @options[:value_max_bytes]
478
- yield
479
- else
480
- message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
481
- raise Dalli::ValueOverMaxSize, message if @options[:error_when_over_max_size]
482
-
483
- Dalli.logger.error "#{message} - this value may be truncated by memcached"
484
- false
485
- end
486
- end
487
-
488
- # https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
489
- # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
490
- MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days
491
- def sanitize_ttl(ttl)
492
- ttl_as_i = ttl.to_i
493
- return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
494
- now = Time.now.to_i
495
- return ttl_as_i if ttl_as_i > now # already a timestamp
496
- Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
497
- now + ttl_as_i
498
- end
499
-
500
- # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
501
- class NilObject; end
502
- NOT_FOUND = NilObject.new
503
-
504
- def generic_response(unpack=false, cache_nils=false)
505
- (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
506
- data = read(count) if count > 0
507
- if status == 1
508
- cache_nils ? NOT_FOUND : nil
509
- elsif status == 2 || status == 5
510
- false # Not stored, normal status for add operation
511
- elsif status != 0
512
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
513
- elsif data
514
- flags = data[0...extras].unpack('N')[0]
515
- value = data[extras..-1]
516
- unpack ? deserialize(value, flags) : value
517
- else
518
- true
519
- end
520
- end
521
-
522
- def cas_response
523
- (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
524
- read(count) if count > 0 # this is potential data that we don't care about
525
- if status == 1
526
- nil
527
- elsif status == 2 || status == 5
528
- false # Not stored, normal status for add operation
529
- elsif status != 0
530
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
531
- else
532
- cas
533
- end
534
- end
535
-
536
- def keyvalue_response
537
- hash = {}
538
- while true
539
- (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
540
- return hash if key_length == 0
541
- key = read(key_length)
542
- value = read(body_length - key_length) if body_length - key_length > 0
543
- hash[key] = value
544
- end
545
- end
546
-
547
- def multi_response
548
- hash = {}
549
- while true
550
- (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
551
- return hash if key_length == 0
552
- flags = read(4).unpack('N')[0]
553
- key = read(key_length)
554
- value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
555
- hash[key] = deserialize(value, flags)
556
- end
557
- end
558
-
559
- def write(bytes)
560
- begin
561
- @inprogress = true
562
- result = @sock.write(bytes)
563
- @inprogress = false
564
- result
565
- rescue SystemCallError, Timeout::Error => e
566
- failure!(e)
567
- end
568
- end
569
-
570
- def read(count)
571
- begin
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
- end
580
-
581
- def read_header
582
- read(24) || raise(Dalli::NetworkError, 'No response')
583
- end
584
-
585
- def connect
586
- Dalli.logger.debug { "Dalli::Server#connect #{name}" }
587
-
588
- begin
589
- @pid = Process.pid
590
- if socket_type == :unix
591
- @sock = KSocket::UNIX.open(hostname, self, options)
592
- else
593
- @sock = KSocket::TCP.open(hostname, port, self, options)
594
- end
595
- sasl_authentication if need_auth?
596
- @version = version # trigger actual connect
597
- up!
598
- rescue Dalli::DalliError # SASL auth failure
599
- raise
600
- rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
601
- # SocketError = DNS resolution failure
602
- failure!(e)
603
- end
604
- end
605
-
606
- def split(n)
607
- [n >> 32, 0xFFFFFFFF & n]
608
- end
609
-
610
- REQUEST = 0x80
611
- RESPONSE = 0x81
612
-
613
- # Response codes taken from:
614
- # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
615
- RESPONSE_CODES = {
616
- 0 => 'No error',
617
- 1 => 'Key not found',
618
- 2 => 'Key exists',
619
- 3 => 'Value too large',
620
- 4 => 'Invalid arguments',
621
- 5 => 'Item not stored',
622
- 6 => 'Incr/decr on a non-numeric value',
623
- 7 => 'The vbucket belongs to another server',
624
- 8 => 'Authentication error',
625
- 9 => 'Authentication continue',
626
- 0x20 => 'Authentication required',
627
- 0x81 => 'Unknown command',
628
- 0x82 => 'Out of memory',
629
- 0x83 => 'Not supported',
630
- 0x84 => 'Internal error',
631
- 0x85 => 'Busy',
632
- 0x86 => 'Temporary failure'
633
- }
634
-
635
- OPCODES = {
636
- :get => 0x00,
637
- :set => 0x01,
638
- :add => 0x02,
639
- :replace => 0x03,
640
- :delete => 0x04,
641
- :incr => 0x05,
642
- :decr => 0x06,
643
- :flush => 0x08,
644
- :noop => 0x0A,
645
- :version => 0x0B,
646
- :getkq => 0x0D,
647
- :append => 0x0E,
648
- :prepend => 0x0F,
649
- :stat => 0x10,
650
- :setq => 0x11,
651
- :addq => 0x12,
652
- :replaceq => 0x13,
653
- :deleteq => 0x14,
654
- :incrq => 0x15,
655
- :decrq => 0x16,
656
- :auth_negotiation => 0x20,
657
- :auth_request => 0x21,
658
- :auth_continue => 0x22,
659
- :touch => 0x1C,
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
- }
682
- FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
683
-
684
-
685
- #######
686
- # SASL authentication support for NorthScale
687
- #######
688
-
689
- def need_auth?
690
- @options[:username] || ENV['MEMCACHE_USERNAME']
691
- end
692
-
693
- def username
694
- @options[:username] || ENV['MEMCACHE_USERNAME']
695
- end
696
-
697
- def password
698
- @options[:password] || ENV['MEMCACHE_PASSWORD']
699
- end
700
-
701
- def sasl_authentication
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).gsub(/\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" if !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 =~ /\A\//
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
- return hostnam, por, weigh, socket_type
750
- end
751
- end
3
+ module Dalli # rubocop:disable Style/Documentation
4
+ warn 'Dalli::Server is deprecated, use Dalli::Protocol::Binary instead'
5
+ Server = Protocol::Binary
752
6
  end