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