dalli 3.0.4 → 3.1.1
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 +31 -0
- data/README.md +25 -134
- data/lib/dalli/cas/client.rb +2 -0
- data/lib/dalli/client.rb +215 -323
- 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 +177 -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 +200 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
- data/lib/dalli/protocol/binary.rb +251 -561
- data/lib/dalli/protocol/connection_manager.rb +242 -0
- data/lib/dalli/protocol/response_buffer.rb +53 -0
- data/lib/dalli/protocol/server_config_parser.rb +22 -5
- 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 +39 -14
- data/lib/rack/session/dalli.rb +95 -76
- metadata +80 -6
@@ -1,703 +1,393 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'forwardable'
|
4
|
+
require 'socket'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
require_relative 'binary/request_formatter'
|
8
|
+
require_relative 'binary/response_header'
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# amount of time to sleep between retries when a failure occurs
|
24
|
-
socket_failure_delay: 0.1,
|
25
|
-
# max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
|
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
|
-
|
37
|
-
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
|
42
|
-
@options = DEFAULTS.merge(options)
|
43
|
-
@value_compressor = ValueCompressor.new(@options)
|
44
|
-
@sock = nil
|
45
|
-
@msg = nil
|
46
|
-
@error = nil
|
47
|
-
@pid = nil
|
48
|
-
@inprogress = nil
|
49
|
-
end
|
50
|
-
|
51
|
-
def name
|
52
|
-
if socket_type == :unix
|
53
|
-
hostname
|
54
|
-
else
|
55
|
-
"#{hostname}:#{port}"
|
56
|
-
end
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
attr_accessor :weight, :options
|
23
|
+
|
24
|
+
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
25
|
+
def_delegators :@connection_manager, :name, :sock, :hostname, :port, :close, :connected?, :socket_timeout,
|
26
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
27
|
+
|
28
|
+
def initialize(attribs, client_options = {})
|
29
|
+
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
30
|
+
@options = client_options.merge(user_creds)
|
31
|
+
@value_marshaller = ValueMarshaller.new(@options)
|
32
|
+
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
33
|
+
@response_processor = ResponseProcessor.new(@connection_manager, @value_marshaller)
|
57
34
|
end
|
58
35
|
|
59
|
-
# Chokepoint method for
|
60
|
-
def request(
|
61
|
-
verify_state
|
62
|
-
|
36
|
+
# Chokepoint method for error handling and ensuring liveness
|
37
|
+
def request(opkey, *args)
|
38
|
+
verify_state(opkey)
|
39
|
+
|
63
40
|
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."
|
41
|
+
send(opkey, *args)
|
42
|
+
rescue Dalli::MarshalError => e
|
43
|
+
log_marshal_err(args.first, e)
|
68
44
|
raise
|
69
|
-
rescue Dalli::DalliError
|
45
|
+
rescue Dalli::DalliError
|
70
46
|
raise
|
71
|
-
rescue =>
|
72
|
-
|
73
|
-
Dalli.logger.error ex.backtrace.join("\n\t")
|
47
|
+
rescue StandardError => e
|
48
|
+
log_unexpected_err(e)
|
74
49
|
down!
|
75
50
|
end
|
76
51
|
end
|
77
52
|
|
53
|
+
##
|
54
|
+
# Boolean method used by clients of this class to determine if this
|
55
|
+
# particular memcached instance is available for use.
|
78
56
|
def alive?
|
79
|
-
|
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
|
86
|
-
|
87
|
-
connect
|
88
|
-
!!@sock
|
57
|
+
ensure_connected!
|
89
58
|
rescue Dalli::NetworkError
|
59
|
+
# ensure_connected! raises a NetworkError if connection fails. We
|
60
|
+
# want to capture that error and convert it to a boolean value here.
|
90
61
|
false
|
91
62
|
end
|
92
63
|
|
93
|
-
def
|
94
|
-
return unless @sock
|
95
|
-
begin
|
96
|
-
@sock.close
|
97
|
-
rescue
|
98
|
-
nil
|
99
|
-
end
|
100
|
-
@sock = nil
|
101
|
-
@pid = nil
|
102
|
-
@inprogress = false
|
103
|
-
end
|
104
|
-
|
105
|
-
def lock!
|
106
|
-
end
|
107
|
-
|
108
|
-
def unlock!
|
109
|
-
end
|
64
|
+
def lock!; end
|
110
65
|
|
111
|
-
def
|
112
|
-
@options[:serializer]
|
113
|
-
end
|
66
|
+
def unlock!; end
|
114
67
|
|
115
68
|
# Start reading key/value pairs from this connection. This is usually called
|
116
69
|
# after a series of GETKQ commands. A NOOP is sent, and the server begins
|
117
70
|
# flushing responses for kv pairs that were found.
|
118
71
|
#
|
119
72
|
# Returns nothing.
|
120
|
-
def
|
121
|
-
verify_state
|
73
|
+
def pipeline_response_setup
|
74
|
+
verify_state(:getkq)
|
122
75
|
write_noop
|
123
|
-
|
124
|
-
@
|
125
|
-
@inprogress = true
|
126
|
-
end
|
127
|
-
|
128
|
-
# Did the last call to #multi_response_start complete successfully?
|
129
|
-
def multi_response_completed?
|
130
|
-
@multi_buffer.nil?
|
76
|
+
response_buffer.reset
|
77
|
+
@connection_manager.start_request!
|
131
78
|
end
|
132
79
|
|
133
80
|
# Attempt to receive and parse as many key/value pairs as possible
|
134
|
-
# from this server. After #
|
81
|
+
# from this server. After #pipeline_response_setup, this should be invoked
|
135
82
|
# repeatedly whenever this server's socket is readable until
|
136
|
-
# #
|
83
|
+
# #pipeline_complete?.
|
137
84
|
#
|
138
85
|
# Returns a Hash of kv pairs received.
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
@multi_buffer << @sock.read_available
|
143
|
-
buf = @multi_buffer
|
144
|
-
pos = @position
|
86
|
+
def pipeline_next_responses
|
87
|
+
reconnect_on_pipeline_complete!
|
145
88
|
values = {}
|
146
89
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
pos = pos + 24 + body_length
|
164
|
-
|
165
|
-
begin
|
166
|
-
values[key] = [deserialize(value, flags), cas]
|
167
|
-
rescue DalliError
|
168
|
-
end
|
169
|
-
|
170
|
-
else
|
171
|
-
# not enough data yet, wait for more
|
172
|
-
break
|
173
|
-
end
|
90
|
+
response_buffer.read
|
91
|
+
|
92
|
+
resp_header, key, value = pipeline_response
|
93
|
+
# resp_header is not nil only if we have a full response to parse
|
94
|
+
# in the buffer
|
95
|
+
while resp_header
|
96
|
+
# If the status is ok and key is nil, then this is the response
|
97
|
+
# to the noop at the end of the pipeline
|
98
|
+
finish_pipeline && break if resp_header.ok? && key.nil?
|
99
|
+
|
100
|
+
# If the status is ok and the key is not nil, then this is a
|
101
|
+
# getkq response with a value that we want to set in the response hash
|
102
|
+
values[key] = [value, resp_header.cas] unless key.nil?
|
103
|
+
|
104
|
+
# Get the next response from the buffer
|
105
|
+
resp_header, key, value = pipeline_response
|
174
106
|
end
|
175
|
-
@position = pos
|
176
107
|
|
177
108
|
values
|
178
109
|
rescue SystemCallError, Timeout::Error, EOFError => e
|
179
|
-
|
110
|
+
@connection_manager.error_on_request!(e)
|
180
111
|
end
|
181
112
|
|
182
|
-
# Abort
|
183
|
-
# timeout.
|
184
|
-
# swallowed.
|
113
|
+
# Abort current pipelined get. Generally used to signal an external
|
114
|
+
# timeout during pipelined get. The underlying socket is
|
115
|
+
# disconnected, and the exception is swallowed.
|
185
116
|
#
|
186
117
|
# Returns nothing.
|
187
|
-
def
|
188
|
-
|
189
|
-
@
|
190
|
-
|
191
|
-
|
118
|
+
def pipeline_abort
|
119
|
+
response_buffer.clear
|
120
|
+
@connection_manager.abort_request!
|
121
|
+
return true unless connected?
|
122
|
+
|
123
|
+
# Closes the connection, which ensures that our connection
|
124
|
+
# is in a clean state for future requests
|
125
|
+
@connection_manager.error_on_request!('External timeout')
|
192
126
|
rescue NetworkError
|
193
127
|
true
|
194
128
|
end
|
195
129
|
|
196
|
-
#
|
130
|
+
# Did the last call to #pipeline_response_setup complete successfully?
|
131
|
+
def pipeline_complete?
|
132
|
+
!response_buffer.in_progress?
|
133
|
+
end
|
197
134
|
|
198
|
-
|
135
|
+
def username
|
136
|
+
@options[:username] || ENV['MEMCACHE_USERNAME']
|
137
|
+
end
|
199
138
|
|
200
|
-
def
|
201
|
-
|
202
|
-
if @pid && @pid != Process.pid
|
203
|
-
message = "Fork detected, re-connecting child process..."
|
204
|
-
Dalli.logger.info { message }
|
205
|
-
reconnect! message
|
206
|
-
end
|
139
|
+
def password
|
140
|
+
@options[:password] || ENV['MEMCACHE_PASSWORD']
|
207
141
|
end
|
208
142
|
|
209
|
-
def
|
210
|
-
|
211
|
-
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
212
|
-
raise Dalli::NetworkError, message
|
143
|
+
def require_auth?
|
144
|
+
!username.nil?
|
213
145
|
end
|
214
146
|
|
215
|
-
|
216
|
-
message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
|
217
|
-
Dalli.logger.warn { message }
|
147
|
+
# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
|
218
148
|
|
219
|
-
|
220
|
-
if @fail_count >= options[:socket_max_failures]
|
221
|
-
down!
|
222
|
-
else
|
223
|
-
reconnect! "Socket operation failed, retrying..."
|
224
|
-
end
|
225
|
-
end
|
149
|
+
private
|
226
150
|
|
227
|
-
|
228
|
-
|
151
|
+
##
|
152
|
+
# Checks to see if we can execute the specified operation. Checks
|
153
|
+
# whether the connection is in use, and whether the command is allowed
|
154
|
+
##
|
155
|
+
def verify_state(opkey)
|
156
|
+
@connection_manager.confirm_ready!
|
157
|
+
verify_allowed_quiet!(opkey) if quiet?
|
229
158
|
|
230
|
-
|
159
|
+
# The ensure_connected call has the side effect of connecting the
|
160
|
+
# underlying socket if it is not connected, or there's been a disconnect
|
161
|
+
# because of timeout or other error. Method raises an error
|
162
|
+
# if it can't connect
|
163
|
+
raise_down_error unless ensure_connected!
|
164
|
+
end
|
231
165
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
166
|
+
# The socket connection to the underlying server is initialized as a side
|
167
|
+
# effect of this call. In fact, this is the ONLY place where that
|
168
|
+
# socket connection is initialized.
|
169
|
+
#
|
170
|
+
# Both this method and connect need to be in this class so we can do auth
|
171
|
+
# as required
|
172
|
+
#
|
173
|
+
# Since this is invoked exclusively in verify_state!, we don't need to worry about
|
174
|
+
# thread safety. Using it elsewhere may require revisiting that assumption.
|
175
|
+
def ensure_connected!
|
176
|
+
return true if connected?
|
177
|
+
return false unless reconnect_down_server?
|
239
178
|
|
240
|
-
|
241
|
-
|
242
|
-
raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
|
179
|
+
connect # This call needs to be in this class so we can do auth
|
180
|
+
connected?
|
243
181
|
end
|
244
182
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
|
249
|
-
end
|
183
|
+
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
184
|
+
def verify_allowed_quiet!(opkey)
|
185
|
+
return if ALLOWED_QUIET_OPS.include?(opkey)
|
250
186
|
|
251
|
-
|
252
|
-
@down_at = nil
|
253
|
-
@last_down_at = nil
|
254
|
-
@msg = nil
|
255
|
-
@error = nil
|
187
|
+
raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
|
256
188
|
end
|
257
189
|
|
258
|
-
def
|
259
|
-
Thread.current[
|
190
|
+
def quiet?
|
191
|
+
Thread.current[::Dalli::QUIET]
|
260
192
|
end
|
261
193
|
|
194
|
+
def cache_nils?(opts)
|
195
|
+
return false unless opts.is_a?(Hash)
|
196
|
+
|
197
|
+
opts[:cache_nils] ? true : false
|
198
|
+
end
|
199
|
+
|
200
|
+
# Retrieval Commands
|
262
201
|
def get(key, options = nil)
|
263
|
-
req =
|
202
|
+
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
264
203
|
write(req)
|
265
|
-
generic_response(true,
|
204
|
+
@response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
|
266
205
|
end
|
267
206
|
|
268
|
-
def
|
269
|
-
|
270
|
-
|
271
|
-
req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
|
272
|
-
end
|
273
|
-
# Could send noop here instead of in multi_response_start
|
207
|
+
def gat(key, ttl, options = nil)
|
208
|
+
ttl = TtlSanitizer.sanitize(ttl)
|
209
|
+
req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
|
274
210
|
write(req)
|
211
|
+
@response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
|
275
212
|
end
|
276
213
|
|
277
|
-
def
|
278
|
-
(value, flags) = serialize(key, value, options)
|
214
|
+
def touch(key, ttl)
|
279
215
|
ttl = TtlSanitizer.sanitize(ttl)
|
216
|
+
write(RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl))
|
217
|
+
@response_processor.generic_response
|
218
|
+
end
|
280
219
|
|
281
|
-
|
282
|
-
|
283
|
-
|
220
|
+
# TODO: This is confusing, as there's a cas command in memcached
|
221
|
+
# and this isn't it. Maybe rename? Maybe eliminate?
|
222
|
+
def cas(key)
|
223
|
+
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
284
224
|
write(req)
|
285
|
-
|
225
|
+
@response_processor.data_cas_response
|
286
226
|
end
|
287
227
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
228
|
+
# Storage Commands
|
229
|
+
def set(key, value, ttl, cas, options)
|
230
|
+
opkey = quiet? ? :setq : :set
|
231
|
+
storage_req(opkey, key, value, ttl, cas, options)
|
232
|
+
end
|
293
233
|
|
294
|
-
|
295
|
-
|
296
|
-
|
234
|
+
def add(key, value, ttl, options)
|
235
|
+
opkey = quiet? ? :addq : :add
|
236
|
+
storage_req(opkey, key, value, ttl, 0, options)
|
297
237
|
end
|
298
238
|
|
299
239
|
def replace(key, value, ttl, cas, options)
|
300
|
-
|
301
|
-
ttl
|
240
|
+
opkey = quiet? ? :replaceq : :replace
|
241
|
+
storage_req(opkey, key, value, ttl, cas, options)
|
242
|
+
end
|
302
243
|
|
303
|
-
|
244
|
+
# rubocop:disable Metrics/ParameterLists
|
245
|
+
def storage_req(opkey, key, value, ttl, cas, options)
|
246
|
+
(value, bitflags) = @value_marshaller.store(key, value, options)
|
247
|
+
ttl = TtlSanitizer.sanitize(ttl)
|
304
248
|
|
305
|
-
req =
|
249
|
+
req = RequestFormatter.standard_request(opkey: opkey, key: key,
|
250
|
+
value: value, bitflags: bitflags,
|
251
|
+
ttl: ttl, cas: cas)
|
306
252
|
write(req)
|
307
|
-
|
253
|
+
@response_processor.storage_response unless quiet?
|
308
254
|
end
|
255
|
+
# rubocop:enable Metrics/ParameterLists
|
309
256
|
|
310
|
-
def
|
311
|
-
|
312
|
-
|
313
|
-
generic_response unless multi?
|
257
|
+
def append(key, value)
|
258
|
+
opkey = quiet? ? :appendq : :append
|
259
|
+
write_append_prepend opkey, key, value
|
314
260
|
end
|
315
261
|
|
316
|
-
def
|
317
|
-
|
318
|
-
|
319
|
-
generic_response
|
262
|
+
def prepend(key, value)
|
263
|
+
opkey = quiet? ? :prependq : :prepend
|
264
|
+
write_append_prepend opkey, key, value
|
320
265
|
end
|
321
266
|
|
322
|
-
def
|
323
|
-
|
324
|
-
|
325
|
-
(h, l) = split(count)
|
326
|
-
(dh, dl) = split(default)
|
327
|
-
req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
|
328
|
-
write(req)
|
329
|
-
body = generic_response
|
330
|
-
body ? body.unpack1("Q>") : body
|
267
|
+
def write_append_prepend(opkey, key, value)
|
268
|
+
write(RequestFormatter.standard_request(opkey: opkey, key: key, value: value))
|
269
|
+
@response_processor.no_body_response unless quiet?
|
331
270
|
end
|
332
271
|
|
333
|
-
|
334
|
-
|
272
|
+
# Delete Commands
|
273
|
+
def delete(key, cas)
|
274
|
+
opkey = quiet? ? :deleteq : :delete
|
275
|
+
req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
|
276
|
+
write(req)
|
277
|
+
@response_processor.no_body_response unless quiet?
|
335
278
|
end
|
336
279
|
|
337
|
-
|
338
|
-
|
280
|
+
# Arithmetic Commands
|
281
|
+
def decr(key, count, ttl, initial)
|
282
|
+
opkey = quiet? ? :decrq : :decr
|
283
|
+
decr_incr opkey, key, count, ttl, initial
|
339
284
|
end
|
340
285
|
|
341
|
-
def
|
342
|
-
|
286
|
+
def incr(key, count, ttl, initial)
|
287
|
+
opkey = quiet? ? :incrq : :incr
|
288
|
+
decr_incr opkey, key, count, ttl, initial
|
343
289
|
end
|
344
290
|
|
345
|
-
|
346
|
-
|
347
|
-
|
291
|
+
# This allows us to special case a nil initial value, and
|
292
|
+
# handle it differently than a zero. This special value
|
293
|
+
# for expiry causes memcached to return a not found
|
294
|
+
# if the key doesn't already exist, rather than
|
295
|
+
# setting the initial value
|
296
|
+
NOT_FOUND_EXPIRY = 0xFFFFFFFF
|
297
|
+
|
298
|
+
def decr_incr(opkey, key, count, ttl, initial)
|
299
|
+
expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
|
300
|
+
initial ||= 0
|
301
|
+
write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
|
302
|
+
count: count, initial: initial, expiry: expiry))
|
303
|
+
@response_processor.decr_incr_response unless quiet?
|
348
304
|
end
|
349
305
|
|
350
|
-
|
351
|
-
|
352
|
-
|
306
|
+
# Other Commands
|
307
|
+
def flush(ttl = 0)
|
308
|
+
opkey = quiet? ? :flushq : :flush
|
309
|
+
write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
|
310
|
+
@response_processor.no_body_response unless quiet?
|
353
311
|
end
|
354
312
|
|
355
313
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
356
314
|
# We need to read all the responses at once.
|
357
315
|
def noop
|
358
316
|
write_noop
|
359
|
-
|
317
|
+
@response_processor.multi_with_keys_response
|
360
318
|
end
|
361
319
|
|
362
|
-
def
|
363
|
-
|
364
|
-
end
|
365
|
-
|
366
|
-
def prepend(key, value)
|
367
|
-
write_append_prepend :prepend, key, value
|
368
|
-
end
|
369
|
-
|
370
|
-
def stats(info = "")
|
371
|
-
req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
|
320
|
+
def stats(info = '')
|
321
|
+
req = RequestFormatter.standard_request(opkey: :stat, key: info)
|
372
322
|
write(req)
|
373
|
-
|
323
|
+
@response_processor.multi_with_keys_response
|
374
324
|
end
|
375
325
|
|
376
326
|
def reset_stats
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
def cas(key)
|
381
|
-
req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
|
382
|
-
write(req)
|
383
|
-
data_cas_response
|
327
|
+
write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
|
328
|
+
@response_processor.generic_response
|
384
329
|
end
|
385
330
|
|
386
331
|
def version
|
387
|
-
|
332
|
+
write(RequestFormatter.standard_request(opkey: :version))
|
333
|
+
@response_processor.generic_response
|
388
334
|
end
|
389
335
|
|
390
|
-
def
|
391
|
-
|
392
|
-
write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
|
393
|
-
end
|
394
|
-
|
395
|
-
def gat(key, ttl, options = nil)
|
396
|
-
ttl = TtlSanitizer.sanitize(ttl)
|
397
|
-
req = [REQUEST, OPCODES[:gat], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:gat])
|
336
|
+
def write_noop
|
337
|
+
req = RequestFormatter.standard_request(opkey: :noop)
|
398
338
|
write(req)
|
399
|
-
generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
|
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
339
|
end
|
494
340
|
|
495
|
-
def
|
496
|
-
|
497
|
-
|
498
|
-
if
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
elsif status != 0
|
503
|
-
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
504
|
-
else
|
505
|
-
cas
|
506
|
-
end
|
341
|
+
def connect
|
342
|
+
@connection_manager.establish_connection
|
343
|
+
authenticate_connection if require_auth?
|
344
|
+
@version = version # Connect socket if not authed
|
345
|
+
up!
|
346
|
+
rescue Dalli::DalliError
|
347
|
+
raise
|
507
348
|
end
|
508
349
|
|
509
|
-
def
|
510
|
-
|
511
|
-
|
512
|
-
(
|
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
|
350
|
+
def pipelined_get(keys)
|
351
|
+
req = +''
|
352
|
+
keys.each do |key|
|
353
|
+
req << RequestFormatter.standard_request(opkey: :getkq, key: key)
|
517
354
|
end
|
355
|
+
# Could send noop here instead of in pipeline_response_setup
|
356
|
+
write(req)
|
518
357
|
end
|
519
358
|
|
520
|
-
def
|
521
|
-
|
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
|
359
|
+
def response_buffer
|
360
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, @response_processor)
|
530
361
|
end
|
531
362
|
|
532
|
-
def
|
533
|
-
|
534
|
-
result = @sock.write(bytes)
|
535
|
-
@inprogress = false
|
536
|
-
result
|
537
|
-
rescue SystemCallError, Timeout::Error => e
|
538
|
-
failure!(e)
|
363
|
+
def pipeline_response
|
364
|
+
response_buffer.process_single_getk_response
|
539
365
|
end
|
540
366
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
rescue SystemCallError, Timeout::Error, EOFError => e
|
547
|
-
failure!(e)
|
548
|
-
end
|
367
|
+
# Called after the noop response is received at the end of a set
|
368
|
+
# of pipelined gets
|
369
|
+
def finish_pipeline
|
370
|
+
response_buffer.clear
|
371
|
+
@connection_manager.finish_request!
|
549
372
|
|
550
|
-
|
551
|
-
read(24) || raise(Dalli::NetworkError, "No response")
|
373
|
+
true # to simplify response
|
552
374
|
end
|
553
375
|
|
554
|
-
def
|
555
|
-
|
556
|
-
|
557
|
-
begin
|
558
|
-
@pid = Process.pid
|
559
|
-
@sock = if socket_type == :unix
|
560
|
-
Dalli::Socket::UNIX.open(hostname, self, options)
|
561
|
-
else
|
562
|
-
Dalli::Socket::TCP.open(hostname, port, self, options)
|
563
|
-
end
|
564
|
-
sasl_authentication if need_auth?
|
565
|
-
@version = version # trigger actual connect
|
566
|
-
up!
|
567
|
-
rescue Dalli::DalliError # SASL auth failure
|
568
|
-
raise
|
569
|
-
rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
|
570
|
-
# SocketError = DNS resolution failure
|
571
|
-
failure!(e)
|
572
|
-
end
|
376
|
+
def reconnect_on_pipeline_complete!
|
377
|
+
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
573
378
|
end
|
574
379
|
|
575
|
-
def
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
REQUEST = 0x80
|
580
|
-
RESPONSE = 0x81
|
581
|
-
|
582
|
-
# Response codes taken from:
|
583
|
-
# https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
|
584
|
-
RESPONSE_CODES = {
|
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"]
|
380
|
+
def log_marshal_err(key, err)
|
381
|
+
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
382
|
+
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
661
383
|
end
|
662
384
|
|
663
|
-
def
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
def password
|
668
|
-
@options[:password] || ENV["MEMCACHE_PASSWORD"]
|
385
|
+
def log_unexpected_err(err)
|
386
|
+
Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
|
387
|
+
Dalli.logger.error err.backtrace.join("\n\t")
|
669
388
|
end
|
670
389
|
|
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
|
390
|
+
include SaslAuthentication
|
701
391
|
end
|
702
392
|
end
|
703
393
|
end
|