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