dalli 2.7.11 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/{History.md → CHANGELOG.md} +146 -0
  3. data/Gemfile +3 -9
  4. data/README.md +27 -201
  5. data/lib/dalli/cas/client.rb +1 -57
  6. data/lib/dalli/client.rb +226 -265
  7. data/lib/dalli/compressor.rb +12 -2
  8. data/lib/dalli/key_manager.rb +121 -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 +121 -0
  20. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  21. data/lib/dalli/protocol/meta.rb +178 -0
  22. data/lib/dalli/protocol/response_buffer.rb +54 -0
  23. data/lib/dalli/protocol/server_config_parser.rb +86 -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 +94 -83
  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 +103 -94
  36. metadata +161 -11
  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