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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +671 -0
- data/Gemfile +15 -3
- data/LICENSE +1 -1
- data/README.md +33 -148
- data/lib/dalli/cas/client.rb +3 -0
- data/lib/dalli/client.rb +293 -131
- data/lib/dalli/compressor.rb +40 -0
- data/lib/dalli/key_manager.rb +121 -0
- data/lib/dalli/options.rb +22 -4
- data/lib/dalli/pid_cache.rb +40 -0
- data/lib/dalli/pipelined_getter.rb +177 -0
- data/lib/dalli/protocol/base.rb +250 -0
- data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
- data/lib/dalli/protocol/binary/response_header.rb +36 -0
- data/lib/dalli/protocol/binary/response_processor.rb +239 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
- data/lib/dalli/protocol/binary.rb +173 -0
- data/lib/dalli/protocol/connection_manager.rb +255 -0
- data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
- data/lib/dalli/protocol/meta/response_processor.rb +211 -0
- data/lib/dalli/protocol/meta.rb +178 -0
- data/lib/dalli/protocol/response_buffer.rb +54 -0
- data/lib/dalli/protocol/server_config_parser.rb +86 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +19 -0
- data/lib/dalli/ring.rb +98 -50
- data/lib/dalli/server.rb +4 -524
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +154 -53
- data/lib/dalli/version.rb +5 -1
- data/lib/dalli.rb +49 -13
- data/lib/rack/session/dalli.rb +169 -26
- metadata +53 -88
- data/History.md +0 -262
- data/Performance.md +0 -42
- data/Rakefile +0 -39
- data/dalli.gemspec +0 -28
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -76
- data/lib/active_support/cache/dalli_store.rb +0 -203
- data/test/abstract_unit.rb +0 -281
- data/test/benchmark_test.rb +0 -187
- data/test/helper.rb +0 -41
- data/test/memcached_mock.rb +0 -113
- data/test/test_active_support.rb +0 -163
- data/test/test_dalli.rb +0 -461
- data/test/test_encoding.rb +0 -43
- data/test/test_failover.rb +0 -107
- data/test/test_network.rb +0 -54
- data/test/test_ring.rb +0 -85
- data/test/test_sasl.rb +0 -83
- data/test/test_session_store.rb +0 -224
data/lib/dalli/server.rb
CHANGED
@@ -1,526 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'timeout'
|
3
|
-
require 'zlib'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
module Dalli
|
6
|
-
|
7
|
-
|
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
|