dalli 2.0.1 → 3.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +671 -0
  3. data/Gemfile +15 -3
  4. data/LICENSE +1 -1
  5. data/README.md +33 -148
  6. data/lib/dalli/cas/client.rb +3 -0
  7. data/lib/dalli/client.rb +293 -131
  8. data/lib/dalli/compressor.rb +40 -0
  9. data/lib/dalli/key_manager.rb +121 -0
  10. data/lib/dalli/options.rb +22 -4
  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 +19 -0
  31. data/lib/dalli/ring.rb +98 -50
  32. data/lib/dalli/server.rb +4 -524
  33. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  34. data/lib/dalli/socket.rb +154 -53
  35. data/lib/dalli/version.rb +5 -1
  36. data/lib/dalli.rb +49 -13
  37. data/lib/rack/session/dalli.rb +169 -26
  38. metadata +53 -88
  39. data/History.md +0 -262
  40. data/Performance.md +0 -42
  41. data/Rakefile +0 -39
  42. data/dalli.gemspec +0 -28
  43. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -76
  44. data/lib/active_support/cache/dalli_store.rb +0 -203
  45. data/test/abstract_unit.rb +0 -281
  46. data/test/benchmark_test.rb +0 -187
  47. data/test/helper.rb +0 -41
  48. data/test/memcached_mock.rb +0 -113
  49. data/test/test_active_support.rb +0 -163
  50. data/test/test_dalli.rb +0 -461
  51. data/test/test_encoding.rb +0 -43
  52. data/test/test_failover.rb +0 -107
  53. data/test/test_network.rb +0 -54
  54. data/test/test_ring.rb +0 -85
  55. data/test/test_sasl.rb +0 -83
  56. data/test/test_session_store.rb +0 -224
data/lib/dalli/server.rb CHANGED
@@ -1,526 +1,6 @@
1
- require 'socket'
2
- require 'timeout'
3
- require 'zlib'
1
+ # frozen_string_literal: true
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
-
12
- DEFAULTS = {
13
- # seconds between trying to contact a remote server
14
- :down_retry_delay => 1,
15
- # connect/read/write timeout for socket operations
16
- :socket_timeout => 0.5,
17
- # times a socket operation may fail before considering the server dead
18
- :socket_max_failures => 2,
19
- # amount of time to sleep between retries when a failure occurs
20
- :socket_failure_delay => 0.01,
21
- # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
22
- :value_max_bytes => 1024 * 1024,
23
- :username => nil,
24
- :password => nil,
25
- :keepalive => true
26
- }
27
-
28
- def initialize(attribs, options = {})
29
- (@hostname, @port, @weight) = attribs.split(':')
30
- @port ||= 11211
31
- @port = Integer(@port)
32
- @weight ||= 1
33
- @weight = Integer(@weight)
34
- @fail_count = 0
35
- @down_at = nil
36
- @last_down_at = nil
37
- @options = DEFAULTS.merge(options)
38
- @sock = nil
39
- @msg = nil
40
- end
41
-
42
- # Chokepoint method for instrumentation
43
- def request(op, *args)
44
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
45
- begin
46
- send(op, *args)
47
- rescue Dalli::NetworkError
48
- raise
49
- rescue Dalli::MarshalError => ex
50
- Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
51
- Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
52
- Dalli.logger.error ex.backtrace.join("\n\t")
53
- false
54
- rescue Dalli::DalliError
55
- raise
56
- rescue => ex
57
- Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
58
- Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
59
- Dalli.logger.error ex.backtrace.join("\n\t")
60
- down!
61
- end
62
- end
63
-
64
- def alive?
65
- return true if @sock
66
-
67
- if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
68
- time = @last_down_at + options[:down_retry_delay] - Time.now
69
- Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
70
- return false
71
- end
72
-
73
- connect
74
- !!@sock
75
- rescue Dalli::NetworkError
76
- false
77
- end
78
-
79
- def close
80
- return unless @sock
81
- @sock.close rescue nil
82
- @sock = nil
83
- end
84
-
85
- def lock!
86
- end
87
-
88
- def unlock!
89
- end
90
-
91
- # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
92
-
93
- private
94
-
95
- def failure!
96
- Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
97
-
98
- @fail_count += 1
99
- if @fail_count >= options[:socket_max_failures]
100
- down!
101
- else
102
- close
103
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
104
- raise Dalli::NetworkError, "Socket operation failed, retrying..."
105
- end
106
- end
107
-
108
- def down!
109
- close
110
-
111
- @last_down_at = Time.now
112
-
113
- if @down_at
114
- time = Time.now - @down_at
115
- Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
116
- else
117
- @down_at = @last_down_at
118
- Dalli.logger.warn { "#{hostname}:#{port} is down" }
119
- end
120
-
121
- @error = $! && $!.class.name
122
- @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
123
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
124
- end
125
-
126
- def up!
127
- if @down_at
128
- time = Time.now - @down_at
129
- Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
130
- end
131
-
132
- @fail_count = 0
133
- @down_at = nil
134
- @last_down_at = nil
135
- @msg = nil
136
- @error = nil
137
- end
138
-
139
- def multi?
140
- Thread.current[:dalli_multi]
141
- end
142
-
143
- def get(key)
144
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
145
- write(req)
146
- generic_response(true)
147
- end
148
-
149
- def getkq(key)
150
- req = [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
151
- write(req)
152
- end
153
-
154
- def set(key, value, ttl, cas, options)
155
- (value, flags) = serialize(key, value, options)
156
-
157
- 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])
158
- write(req)
159
- generic_response unless multi?
160
- end
161
-
162
- def add(key, value, ttl, options)
163
- (value, flags) = serialize(key, value, options)
164
-
165
- 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])
166
- write(req)
167
- generic_response unless multi?
168
- end
169
-
170
- def replace(key, value, ttl, options)
171
- (value, flags) = serialize(key, value, options)
172
- req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:replace])
173
- write(req)
174
- generic_response unless multi?
175
- end
176
-
177
- def delete(key)
178
- req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:delete])
179
- write(req)
180
- generic_response unless multi?
181
- end
182
-
183
- def flush(ttl)
184
- req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
185
- write(req)
186
- generic_response
187
- end
188
-
189
- def decr(key, count, ttl, default)
190
- expiry = default ? ttl : 0xFFFFFFFF
191
- default ||= 0
192
- (h, l) = split(count)
193
- (dh, dl) = split(default)
194
- req = [REQUEST, OPCODES[:decr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
195
- write(req)
196
- body = generic_response
197
- body ? longlong(*body.unpack('NN')) : body
198
- end
199
-
200
- def incr(key, count, ttl, default)
201
- expiry = default ? ttl : 0xFFFFFFFF
202
- default ||= 0
203
- (h, l) = split(count)
204
- (dh, dl) = split(default)
205
- req = [REQUEST, OPCODES[:incr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
206
- write(req)
207
- body = generic_response
208
- body ? longlong(*body.unpack('NN')) : body
209
- end
210
-
211
- # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
212
- # We need to read all the responses at once.
213
- def noop
214
- req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
215
- write(req)
216
- multi_response
217
- end
218
-
219
- def append(key, value)
220
- req = [REQUEST, OPCODES[:append], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:append])
221
- write(req)
222
- generic_response
223
- end
224
-
225
- def prepend(key, value)
226
- req = [REQUEST, OPCODES[:prepend], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:prepend])
227
- write(req)
228
- generic_response
229
- end
230
-
231
- def stats(info='')
232
- req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
233
- write(req)
234
- keyvalue_response
235
- end
236
-
237
- def reset_stats
238
- req = [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
239
- write(req)
240
- generic_response
241
- end
242
-
243
- def cas(key)
244
- req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
245
- write(req)
246
- cas_response
247
- end
248
-
249
- def version
250
- req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
251
- write(req)
252
- generic_response
253
- end
254
-
255
- COMPRESSION_MIN_SIZE = 1024
256
-
257
- # http://www.hjp.at/zettel/m/memcached_flags.rxml
258
- # Looks like most clients use bit 0 to indicate native language serialization
259
- # and bit 1 to indicate gzip compression.
260
- FLAG_MARSHALLED = 0x1
261
- FLAG_COMPRESSED = 0x2
262
-
263
- def serialize(key, value, options=nil)
264
- marshalled = false
265
- value = unless options && options[:raw]
266
- marshalled = true
267
- begin
268
- Marshal.dump(value)
269
- rescue => ex
270
- # Marshalling can throw several different types of generic Ruby exceptions.
271
- # Convert to a specific exception so we can special case it higher up the stack.
272
- exc = Dalli::MarshalError.new(ex.message)
273
- exc.set_backtrace ex.backtrace
274
- raise exc
275
- end
276
- else
277
- value.to_s
278
- end
279
- compressed = false
280
- if @options[:compress] && value.bytesize >= COMPRESSION_MIN_SIZE
281
- value = Zlib::Deflate.deflate(value)
282
- compressed = true
283
- end
284
- raise Dalli::DalliError, "Value too large, memcached can only store #{@options[:value_max_bytes]} bytes per key [key: #{key}, size: #{value.bytesize}]" if value.bytesize > @options[:value_max_bytes]
285
- flags = 0
286
- flags |= FLAG_COMPRESSED if compressed
287
- flags |= FLAG_MARSHALLED if marshalled
288
- [value, flags]
289
- end
290
-
291
- def deserialize(value, flags)
292
- value = Zlib::Inflate.inflate(value) if (flags & FLAG_COMPRESSED) != 0
293
- value = Marshal.load(value) if (flags & FLAG_MARSHALLED) != 0
294
- value
295
- rescue TypeError, ArgumentError
296
- raise DalliError, "Unable to unmarshal value: #{$!.message}"
297
- rescue Zlib::Error
298
- raise DalliError, "Unable to uncompress value: #{$!.message}"
299
- end
300
-
301
- def cas_response
302
- header = read(24)
303
- raise Dalli::NetworkError, 'No response' if !header
304
- (extras, type, status, count, _, cas) = header.unpack(CAS_HEADER)
305
- data = read(count) if count > 0
306
- if status == 1
307
- nil
308
- elsif status != 0
309
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
310
- elsif data
311
- flags = data[0...extras].unpack('N')[0]
312
- value = data[extras..-1]
313
- data = deserialize(value, flags)
314
- end
315
- [data, cas]
316
- end
317
-
318
- CAS_HEADER = '@4CCnNNQ'
319
- NORMAL_HEADER = '@4CCnN'
320
- KV_HEADER = '@2n@6nN'
321
-
322
- def generic_response(unpack=false)
323
- header = read(24)
324
- raise Dalli::NetworkError, 'No response' if !header
325
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
326
- data = read(count) if count > 0
327
- if status == 1
328
- nil
329
- elsif status == 2 || status == 5
330
- false # Not stored, normal status for add operation
331
- elsif status != 0
332
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
333
- elsif data
334
- flags = data[0...extras].unpack('N')[0]
335
- value = data[extras..-1]
336
- unpack ? deserialize(value, flags) : value
337
- else
338
- true
339
- end
340
- end
341
-
342
- def keyvalue_response
343
- hash = {}
344
- loop do
345
- header = read(24)
346
- raise Dalli::NetworkError, 'No response' if !header
347
- (key_length, status, body_length) = header.unpack(KV_HEADER)
348
- return hash if key_length == 0
349
- key = read(key_length)
350
- value = read(body_length - key_length) if body_length - key_length > 0
351
- hash[key] = value
352
- end
353
- end
354
-
355
- def multi_response
356
- hash = {}
357
- loop do
358
- header = read(24)
359
- raise Dalli::NetworkError, 'No response' if !header
360
- (key_length, status, body_length) = header.unpack(KV_HEADER)
361
- return hash if key_length == 0
362
- flags = read(4).unpack('N')[0]
363
- key = read(key_length)
364
- value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
365
- hash[key] = deserialize(value, flags)
366
- end
367
- end
368
-
369
- def write(bytes)
370
- begin
371
- @sock.write(bytes)
372
- rescue SystemCallError, Timeout::Error
373
- failure!
374
- retry
375
- end
376
- end
377
-
378
- def read(count)
379
- begin
380
- @sock.readfull(count)
381
- rescue SystemCallError, Timeout::Error, EOFError
382
- failure!
383
- retry
384
- end
385
- end
386
-
387
- def connect
388
- Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
389
-
390
- begin
391
- @sock = KSocket.open(hostname, port, options)
392
- @version = version # trigger actual connect
393
- sasl_authentication if need_auth?
394
- up!
395
- rescue Dalli::DalliError # SASL auth failure
396
- raise
397
- rescue SystemCallError, Timeout::Error, EOFError, SocketError
398
- # SocketError = DNS resolution failure
399
- failure!
400
- retry
401
- end
402
- end
403
-
404
- def split(n)
405
- [n >> 32, 0xFFFFFFFF & n]
406
- end
407
-
408
- def longlong(a, b)
409
- (a << 32) | b
410
- end
411
-
412
- REQUEST = 0x80
413
- RESPONSE = 0x81
414
-
415
- RESPONSE_CODES = {
416
- 0 => 'No error',
417
- 1 => 'Key not found',
418
- 2 => 'Key exists',
419
- 3 => 'Value too large',
420
- 4 => 'Invalid arguments',
421
- 5 => 'Item not stored',
422
- 6 => 'Incr/decr on a non-numeric value',
423
- 0x20 => 'Authentication required',
424
- 0x81 => 'Unknown command',
425
- 0x82 => 'Out of memory',
426
- }
427
-
428
- OPCODES = {
429
- :get => 0x00,
430
- :set => 0x01,
431
- :add => 0x02,
432
- :replace => 0x03,
433
- :delete => 0x04,
434
- :incr => 0x05,
435
- :decr => 0x06,
436
- :flush => 0x08,
437
- :noop => 0x0A,
438
- :version => 0x0B,
439
- :getkq => 0x0D,
440
- :append => 0x0E,
441
- :prepend => 0x0F,
442
- :stat => 0x10,
443
- :setq => 0x11,
444
- :addq => 0x12,
445
- :replaceq => 0x13,
446
- :deleteq => 0x14,
447
- :incrq => 0x15,
448
- :decrq => 0x16,
449
- :auth_negotiation => 0x20,
450
- :auth_request => 0x21,
451
- :auth_continue => 0x22,
452
- }
453
-
454
- HEADER = "CCnCCnNNQ"
455
- OP_FORMAT = {
456
- :get => 'a*',
457
- :set => 'NNa*a*',
458
- :add => 'NNa*a*',
459
- :replace => 'NNa*a*',
460
- :delete => 'a*',
461
- :incr => 'NNNNNa*',
462
- :decr => 'NNNNNa*',
463
- :flush => 'N',
464
- :noop => '',
465
- :getkq => 'a*',
466
- :version => '',
467
- :stat => 'a*',
468
- :append => 'a*a*',
469
- :prepend => 'a*a*',
470
- :auth_request => 'a*a*',
471
- :auth_continue => 'a*a*',
472
- }
473
- FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
474
-
475
-
476
- #######
477
- # SASL authentication support for NorthScale
478
- #######
479
-
480
- def need_auth?
481
- @options[:username] || ENV['MEMCACHE_USERNAME']
482
- end
483
-
484
- def username
485
- @options[:username] || ENV['MEMCACHE_USERNAME']
486
- end
487
-
488
- def password
489
- @options[:password] || ENV['MEMCACHE_PASSWORD']
490
- end
491
-
492
- def sasl_authentication
493
- Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
494
-
495
- # negotiate
496
- req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
497
- write(req)
498
- header = read(24)
499
- raise Dalli::NetworkError, 'No response' if !header
500
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
501
- raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
502
- content = read(count)
503
- return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
504
- mechanisms = content.split(' ')
505
- raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
506
-
507
- # request
508
- mechanism = 'PLAIN'
509
- msg = "\x0#{username}\x0#{password}"
510
- req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
511
- write(req)
512
-
513
- header = read(24)
514
- raise Dalli::NetworkError, 'No response' if !header
515
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
516
- raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
517
- content = read(count)
518
- return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
519
-
520
- raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
521
- raise NotImplementedError, "No two-step authentication mechanisms supported"
522
- # (step, msg) = sasl.receive('challenge', content)
523
- # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
524
- end
525
- end
3
+ module Dalli # rubocop:disable Style/Documentation
4
+ warn 'Dalli::Server is deprecated, use Dalli::Protocol::Binary instead'
5
+ Server = Protocol::Binary
526
6
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # This module contains methods for validating and normalizing the servers
6
+ # argument passed to the client. This argument can be nil, a string, or
7
+ # an array of strings. Each string value in the argument can represent
8
+ # a single server or a comma separated list of servers.
9
+ #
10
+ # If nil, it falls back to the values of ENV['MEMCACHE_SERVERS'] if the latter is
11
+ # defined. If that environment value is not defined, a default of '127.0.0.1:11211'
12
+ # is used.
13
+ #
14
+ # A server config string can take one of three forms:
15
+ # * A colon separated string of (host, port, weight) where both port and
16
+ # weight are optional (e.g. 'localhost', 'abc.com:12345', 'example.org:22222:3')
17
+ # * A colon separated string of (UNIX socket, weight) where the weight is optional
18
+ # (e.g. '/var/run/memcached/socket', '/tmp/xyz:3') (not supported on Windows)
19
+ # * A URI with a 'memcached' protocol, which will typically include a username/password
20
+ #
21
+ # The methods in this module do not validate the format of individual server strings, but
22
+ # rather normalize the argument into a compact array, wherein each array entry corresponds
23
+ # to a single server config string. If that normalization is not possible, then an
24
+ # ArgumentError is thrown.
25
+ ##
26
+ module ServersArgNormalizer
27
+ ENV_VAR_NAME = 'MEMCACHE_SERVERS'
28
+ DEFAULT_SERVERS = ['127.0.0.1:11211'].freeze
29
+
30
+ ##
31
+ # Normalizes the argument into an array of servers.
32
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
33
+ # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
34
+ def self.normalize_servers(arg)
35
+ arg = apply_defaults(arg)
36
+ validate_type(arg)
37
+ Array(arg).flat_map { |s| s.split(',') }.reject(&:empty?)
38
+ end
39
+
40
+ def self.apply_defaults(arg)
41
+ return arg unless arg.nil?
42
+
43
+ ENV.fetch(ENV_VAR_NAME, nil) || DEFAULT_SERVERS
44
+ end
45
+
46
+ def self.validate_type(arg)
47
+ return if arg.is_a?(String)
48
+ return if arg.is_a?(Array) && arg.all?(String)
49
+
50
+ raise ArgumentError,
51
+ 'An explicit servers argument must be a comma separated string or an array containing strings.'
52
+ end
53
+ end
54
+ end