dalli 2.7.4 → 3.2.5

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