dalli 1.1.4 → 2.7.11
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 +7 -0
- data/Gemfile +8 -4
- data/History.md +267 -1
- data/LICENSE +1 -1
- data/README.md +129 -85
- data/lib/action_dispatch/middleware/session/dalli_store.rb +7 -1
- data/lib/active_support/cache/dalli_store.rb +361 -104
- data/lib/dalli.rb +9 -7
- data/lib/dalli/cas/client.rb +59 -0
- data/lib/dalli/client.rb +298 -103
- data/lib/dalli/compressor.rb +30 -0
- data/lib/dalli/options.rb +19 -0
- data/lib/dalli/railtie.rb +8 -0
- data/lib/dalli/ring.rb +59 -22
- data/lib/dalli/server.rb +346 -125
- data/lib/dalli/socket.rb +105 -105
- data/lib/dalli/version.rb +2 -1
- data/lib/rack/session/dalli.rb +156 -22
- metadata +29 -104
- data/Performance.md +0 -85
- data/Rakefile +0 -39
- data/Upgrade.md +0 -45
- data/dalli.gemspec +0 -31
- data/lib/active_support/cache/dalli_store23.rb +0 -172
- data/lib/dalli/compatibility.rb +0 -52
- data/lib/dalli/memcache-client.rb +0 -1
- data/test/abstract_unit.rb +0 -282
- data/test/benchmark_test.rb +0 -170
- data/test/helper.rb +0 -39
- data/test/memcached_mock.rb +0 -126
- data/test/test_active_support.rb +0 -201
- data/test/test_compatibility.rb +0 -33
- data/test/test_dalli.rb +0 -450
- data/test/test_encoding.rb +0 -43
- data/test/test_failover.rb +0 -107
- data/test/test_network.rb +0 -54
- data/test/test_ring.rb +0 -85
- data/test/test_sasl.rb +0 -83
- data/test/test_session_store.rb +0 -225
- data/test/test_synchrony.rb +0 -175
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'zlib'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Dalli
|
6
|
+
class Compressor
|
7
|
+
def self.compress(data)
|
8
|
+
Zlib::Deflate.deflate(data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.decompress(data)
|
12
|
+
Zlib::Inflate.inflate(data)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class GzipCompressor
|
17
|
+
def self.compress(data)
|
18
|
+
io = StringIO.new(String.new(""), "w")
|
19
|
+
gz = Zlib::GzipWriter.new(io)
|
20
|
+
gz.write(data)
|
21
|
+
gz.close
|
22
|
+
io.string
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.decompress(data)
|
26
|
+
io = StringIO.new(data, "rb")
|
27
|
+
Zlib::GzipReader.new(io).read
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dalli/options.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'thread'
|
2
3
|
require 'monitor'
|
3
4
|
|
@@ -31,6 +32,24 @@ module Dalli
|
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
35
|
+
def multi_response_start
|
36
|
+
@lock.synchronize do
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def multi_response_nonblock
|
42
|
+
@lock.synchronize do
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def multi_response_abort
|
48
|
+
@lock.synchronize do
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
34
53
|
def lock!
|
35
54
|
@lock.mon_enter
|
36
55
|
end
|
data/lib/dalli/ring.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'digest/sha1'
|
2
3
|
require 'zlib'
|
3
4
|
|
@@ -15,12 +16,12 @@ module Dalli
|
|
15
16
|
continuum = []
|
16
17
|
servers.each do |server|
|
17
18
|
entry_count_for(server, servers.size, total_weight).times do |idx|
|
18
|
-
hash = Digest::SHA1.hexdigest("#{server.
|
19
|
+
hash = Digest::SHA1.hexdigest("#{server.name}:#{idx}")
|
19
20
|
value = Integer("0x#{hash[0..7]}")
|
20
21
|
continuum << Dalli::Ring::Entry.new(value, server)
|
21
22
|
end
|
22
23
|
end
|
23
|
-
@continuum = continuum.
|
24
|
+
@continuum = continuum.sort_by(&:value)
|
24
25
|
end
|
25
26
|
|
26
27
|
threadsafe! unless options[:threadsafe] == false
|
@@ -31,7 +32,7 @@ module Dalli
|
|
31
32
|
if @continuum
|
32
33
|
hkey = hash_for(key)
|
33
34
|
20.times do |try|
|
34
|
-
entryidx =
|
35
|
+
entryidx = binary_search(@continuum, hkey)
|
35
36
|
server = @continuum[entryidx].server
|
36
37
|
return server if server.alive?
|
37
38
|
break unless @failover
|
@@ -46,11 +47,11 @@ module Dalli
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def lock
|
49
|
-
@servers.each
|
50
|
+
@servers.each(&:lock!)
|
50
51
|
begin
|
51
52
|
return yield
|
52
53
|
ensure
|
53
|
-
@servers.each
|
54
|
+
@servers.each(&:unlock!)
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
@@ -70,25 +71,61 @@ module Dalli
|
|
70
71
|
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
71
72
|
end
|
72
73
|
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
74
|
+
# Native extension to perform the binary search within the continuum
|
75
|
+
# space. Fallback to a pure Ruby version if the compilation doesn't work.
|
76
|
+
# optional for performance and only necessary if you are using multiple
|
77
|
+
# memcached servers.
|
78
|
+
begin
|
79
|
+
require 'inline'
|
80
|
+
inline do |builder|
|
81
|
+
builder.c <<-EOM
|
82
|
+
int binary_search(VALUE ary, unsigned int r) {
|
83
|
+
long upper = RARRAY_LEN(ary) - 1;
|
84
|
+
long lower = 0;
|
85
|
+
long idx = 0;
|
86
|
+
ID value = rb_intern("value");
|
87
|
+
VALUE continuumValue;
|
88
|
+
unsigned int l;
|
89
|
+
|
90
|
+
while (lower <= upper) {
|
91
|
+
idx = (lower + upper) / 2;
|
92
|
+
|
93
|
+
continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0);
|
94
|
+
l = NUM2UINT(continuumValue);
|
95
|
+
if (l == r) {
|
96
|
+
return idx;
|
97
|
+
}
|
98
|
+
else if (l > r) {
|
99
|
+
upper = idx - 1;
|
100
|
+
}
|
101
|
+
else {
|
102
|
+
lower = idx + 1;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
return upper;
|
106
|
+
}
|
107
|
+
EOM
|
108
|
+
end
|
109
|
+
rescue LoadError
|
110
|
+
# Find the closest index in the Ring with value <= the given value
|
111
|
+
def binary_search(ary, value)
|
112
|
+
upper = ary.size - 1
|
113
|
+
lower = 0
|
114
|
+
|
115
|
+
while (lower <= upper) do
|
116
|
+
idx = (lower + upper) / 2
|
117
|
+
comp = ary[idx].value <=> value
|
118
|
+
|
119
|
+
if comp == 0
|
120
|
+
return idx
|
121
|
+
elsif comp > 0
|
122
|
+
upper = idx - 1
|
123
|
+
else
|
124
|
+
lower = idx + 1
|
125
|
+
end
|
89
126
|
end
|
127
|
+
upper
|
90
128
|
end
|
91
|
-
return upper
|
92
129
|
end
|
93
130
|
|
94
131
|
class Entry
|
data/lib/dalli/server.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'socket'
|
2
3
|
require 'timeout'
|
3
|
-
require 'zlib'
|
4
4
|
|
5
5
|
module Dalli
|
6
6
|
class Server
|
@@ -8,10 +8,14 @@ module Dalli
|
|
8
8
|
attr_accessor :port
|
9
9
|
attr_accessor :weight
|
10
10
|
attr_accessor :options
|
11
|
+
attr_reader :sock
|
12
|
+
attr_reader :socket_type # possible values: :unix, :tcp
|
11
13
|
|
14
|
+
DEFAULT_PORT = 11211
|
15
|
+
DEFAULT_WEIGHT = 1
|
12
16
|
DEFAULTS = {
|
13
17
|
# seconds between trying to contact a remote server
|
14
|
-
:down_retry_delay =>
|
18
|
+
:down_retry_delay => 60,
|
15
19
|
# connect/read/write timeout for socket operations
|
16
20
|
:socket_timeout => 0.5,
|
17
21
|
# times a socket operation may fail before considering the server dead
|
@@ -20,42 +24,59 @@ module Dalli
|
|
20
24
|
:socket_failure_delay => 0.01,
|
21
25
|
# max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
|
22
26
|
:value_max_bytes => 1024 * 1024,
|
27
|
+
# surpassing value_max_bytes either warns (false) or throws (true)
|
28
|
+
:error_when_over_max_size => false,
|
29
|
+
:compressor => Compressor,
|
30
|
+
# min byte size to attempt compression
|
31
|
+
:compression_min_size => 1024,
|
32
|
+
# max byte size for compression
|
33
|
+
:compression_max_size => false,
|
34
|
+
:serializer => Marshal,
|
23
35
|
:username => nil,
|
24
36
|
:password => nil,
|
25
|
-
:
|
37
|
+
:keepalive => true,
|
38
|
+
# max byte size for SO_SNDBUF
|
39
|
+
:sndbuf => nil,
|
40
|
+
# max byte size for SO_RCVBUF
|
41
|
+
:rcvbuf => nil
|
26
42
|
}
|
27
43
|
|
28
44
|
def initialize(attribs, options = {})
|
29
|
-
|
30
|
-
@port ||= 11211
|
31
|
-
@port = Integer(@port)
|
32
|
-
@weight ||= 1
|
33
|
-
@weight = Integer(@weight)
|
45
|
+
@hostname, @port, @weight, @socket_type = parse_hostname(attribs)
|
34
46
|
@fail_count = 0
|
35
47
|
@down_at = nil
|
36
48
|
@last_down_at = nil
|
37
49
|
@options = DEFAULTS.merge(options)
|
38
50
|
@sock = nil
|
39
51
|
@msg = nil
|
52
|
+
@error = nil
|
53
|
+
@pid = nil
|
54
|
+
@inprogress = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
if socket_type == :unix
|
59
|
+
hostname
|
60
|
+
else
|
61
|
+
"#{hostname}:#{port}"
|
62
|
+
end
|
40
63
|
end
|
41
64
|
|
42
65
|
# Chokepoint method for instrumentation
|
43
66
|
def request(op, *args)
|
44
|
-
|
67
|
+
verify_state
|
68
|
+
raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
|
45
69
|
begin
|
46
70
|
send(op, *args)
|
47
|
-
rescue Dalli::NetworkError
|
48
|
-
raise
|
49
71
|
rescue Dalli::MarshalError => ex
|
50
72
|
Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
|
51
73
|
Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
|
52
74
|
Dalli.logger.error ex.backtrace.join("\n\t")
|
53
75
|
false
|
54
|
-
rescue Dalli::DalliError
|
76
|
+
rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
|
55
77
|
raise
|
56
78
|
rescue => ex
|
57
|
-
Dalli.logger.error "Unexpected exception
|
58
|
-
Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
|
79
|
+
Dalli.logger.error "Unexpected exception during Dalli request: #{ex.class.name}: #{ex.message}"
|
59
80
|
Dalli.logger.error ex.backtrace.join("\n\t")
|
60
81
|
down!
|
61
82
|
end
|
@@ -66,7 +87,7 @@ module Dalli
|
|
66
87
|
|
67
88
|
if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
|
68
89
|
time = @last_down_at + options[:down_retry_delay] - Time.now
|
69
|
-
Dalli.logger.debug { "down_retry_delay not reached for #{
|
90
|
+
Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
|
70
91
|
return false
|
71
92
|
end
|
72
93
|
|
@@ -80,6 +101,8 @@ module Dalli
|
|
80
101
|
return unless @sock
|
81
102
|
@sock.close rescue nil
|
82
103
|
@sock = nil
|
104
|
+
@pid = nil
|
105
|
+
@inprogress = false
|
83
106
|
end
|
84
107
|
|
85
108
|
def lock!
|
@@ -88,24 +111,123 @@ module Dalli
|
|
88
111
|
def unlock!
|
89
112
|
end
|
90
113
|
|
114
|
+
def serializer
|
115
|
+
@options[:serializer]
|
116
|
+
end
|
117
|
+
|
118
|
+
def compressor
|
119
|
+
@options[:compressor]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Start reading key/value pairs from this connection. This is usually called
|
123
|
+
# after a series of GETKQ commands. A NOOP is sent, and the server begins
|
124
|
+
# flushing responses for kv pairs that were found.
|
125
|
+
#
|
126
|
+
# Returns nothing.
|
127
|
+
def multi_response_start
|
128
|
+
verify_state
|
129
|
+
write_noop
|
130
|
+
@multi_buffer = String.new('')
|
131
|
+
@position = 0
|
132
|
+
@inprogress = true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Did the last call to #multi_response_start complete successfully?
|
136
|
+
def multi_response_completed?
|
137
|
+
@multi_buffer.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
# Attempt to receive and parse as many key/value pairs as possible
|
141
|
+
# from this server. After #multi_response_start, this should be invoked
|
142
|
+
# repeatedly whenever this server's socket is readable until
|
143
|
+
# #multi_response_completed?.
|
144
|
+
#
|
145
|
+
# Returns a Hash of kv pairs received.
|
146
|
+
def multi_response_nonblock
|
147
|
+
raise 'multi_response has completed' if @multi_buffer.nil?
|
148
|
+
|
149
|
+
@multi_buffer << @sock.read_available
|
150
|
+
buf = @multi_buffer
|
151
|
+
pos = @position
|
152
|
+
values = {}
|
153
|
+
|
154
|
+
while buf.bytesize - pos >= 24
|
155
|
+
header = buf.slice(pos, 24)
|
156
|
+
(key_length, _, body_length, cas) = header.unpack(KV_HEADER)
|
157
|
+
|
158
|
+
if key_length == 0
|
159
|
+
# all done!
|
160
|
+
@multi_buffer = nil
|
161
|
+
@position = nil
|
162
|
+
@inprogress = false
|
163
|
+
break
|
164
|
+
|
165
|
+
elsif buf.bytesize - pos >= 24 + body_length
|
166
|
+
flags = buf.slice(pos + 24, 4).unpack('N')[0]
|
167
|
+
key = buf.slice(pos + 24 + 4, key_length)
|
168
|
+
value = buf.slice(pos + 24 + 4 + key_length, body_length - key_length - 4) if body_length - key_length - 4 > 0
|
169
|
+
|
170
|
+
pos = pos + 24 + body_length
|
171
|
+
|
172
|
+
begin
|
173
|
+
values[key] = [deserialize(value, flags), cas]
|
174
|
+
rescue DalliError
|
175
|
+
end
|
176
|
+
|
177
|
+
else
|
178
|
+
# not enough data yet, wait for more
|
179
|
+
break
|
180
|
+
end
|
181
|
+
end
|
182
|
+
@position = pos
|
183
|
+
|
184
|
+
values
|
185
|
+
rescue SystemCallError, Timeout::Error, EOFError => e
|
186
|
+
failure!(e)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Abort an earlier #multi_response_start. Used to signal an external
|
190
|
+
# timeout. The underlying socket is disconnected, and the exception is
|
191
|
+
# swallowed.
|
192
|
+
#
|
193
|
+
# Returns nothing.
|
194
|
+
def multi_response_abort
|
195
|
+
@multi_buffer = nil
|
196
|
+
@position = nil
|
197
|
+
@inprogress = false
|
198
|
+
failure!(RuntimeError.new('External timeout'))
|
199
|
+
rescue NetworkError
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
91
203
|
# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
|
92
204
|
|
93
205
|
private
|
94
206
|
|
95
|
-
def
|
96
|
-
|
207
|
+
def verify_state
|
208
|
+
failure!(RuntimeError.new('Already writing to socket')) if @inprogress
|
209
|
+
if @pid && @pid != Process.pid
|
210
|
+
message = 'Fork detected, re-connecting child process...'
|
211
|
+
Dalli.logger.info { message }
|
212
|
+
reconnect! message
|
213
|
+
end
|
97
214
|
end
|
98
215
|
|
99
|
-
def
|
100
|
-
|
216
|
+
def reconnect!(message)
|
217
|
+
close
|
218
|
+
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
219
|
+
raise Dalli::NetworkError, message
|
220
|
+
end
|
221
|
+
|
222
|
+
def failure!(exception)
|
223
|
+
message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
|
224
|
+
Dalli.logger.warn { message }
|
101
225
|
|
102
226
|
@fail_count += 1
|
103
227
|
if @fail_count >= options[:socket_max_failures]
|
104
228
|
down!
|
105
229
|
else
|
106
|
-
|
107
|
-
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
108
|
-
raise Dalli::NetworkError, "Socket operation failed, retrying..."
|
230
|
+
reconnect! 'Socket operation failed, retrying...'
|
109
231
|
end
|
110
232
|
end
|
111
233
|
|
@@ -116,21 +238,21 @@ module Dalli
|
|
116
238
|
|
117
239
|
if @down_at
|
118
240
|
time = Time.now - @down_at
|
119
|
-
Dalli.logger.debug { "#{
|
241
|
+
Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
|
120
242
|
else
|
121
243
|
@down_at = @last_down_at
|
122
|
-
Dalli.logger.warn { "#{
|
244
|
+
Dalli.logger.warn { "#{name} is down" }
|
123
245
|
end
|
124
246
|
|
125
247
|
@error = $! && $!.class.name
|
126
248
|
@msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
|
127
|
-
raise Dalli::NetworkError, "#{
|
249
|
+
raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
|
128
250
|
end
|
129
251
|
|
130
252
|
def up!
|
131
253
|
if @down_at
|
132
254
|
time = Time.now - @down_at
|
133
|
-
Dalli.logger.warn { "#{
|
255
|
+
Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
|
134
256
|
end
|
135
257
|
|
136
258
|
@fail_count = 0
|
@@ -144,42 +266,56 @@ module Dalli
|
|
144
266
|
Thread.current[:dalli_multi]
|
145
267
|
end
|
146
268
|
|
147
|
-
def get(key)
|
269
|
+
def get(key, options=nil)
|
148
270
|
req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
|
149
271
|
write(req)
|
150
|
-
generic_response(true)
|
272
|
+
generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
|
151
273
|
end
|
152
274
|
|
153
|
-
def
|
154
|
-
req =
|
275
|
+
def send_multiget(keys)
|
276
|
+
req = String.new("")
|
277
|
+
keys.each do |key|
|
278
|
+
req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
|
279
|
+
end
|
280
|
+
# Could send noop here instead of in multi_response_start
|
155
281
|
write(req)
|
156
282
|
end
|
157
283
|
|
158
284
|
def set(key, value, ttl, cas, options)
|
159
285
|
(value, flags) = serialize(key, value, options)
|
286
|
+
ttl = sanitize_ttl(ttl)
|
160
287
|
|
161
|
-
|
162
|
-
|
163
|
-
|
288
|
+
guard_max_value(key, value) do
|
289
|
+
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])
|
290
|
+
write(req)
|
291
|
+
cas_response unless multi?
|
292
|
+
end
|
164
293
|
end
|
165
294
|
|
166
295
|
def add(key, value, ttl, options)
|
167
296
|
(value, flags) = serialize(key, value, options)
|
297
|
+
ttl = sanitize_ttl(ttl)
|
168
298
|
|
169
|
-
|
170
|
-
|
171
|
-
|
299
|
+
guard_max_value(key, value) do
|
300
|
+
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])
|
301
|
+
write(req)
|
302
|
+
cas_response unless multi?
|
303
|
+
end
|
172
304
|
end
|
173
305
|
|
174
|
-
def replace(key, value, ttl, options)
|
306
|
+
def replace(key, value, ttl, cas, options)
|
175
307
|
(value, flags) = serialize(key, value, options)
|
176
|
-
|
177
|
-
|
178
|
-
|
308
|
+
ttl = sanitize_ttl(ttl)
|
309
|
+
|
310
|
+
guard_max_value(key, value) do
|
311
|
+
req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:replace])
|
312
|
+
write(req)
|
313
|
+
cas_response unless multi?
|
314
|
+
end
|
179
315
|
end
|
180
316
|
|
181
|
-
def delete(key)
|
182
|
-
req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0,
|
317
|
+
def delete(key, cas)
|
318
|
+
req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, cas, key].pack(FORMAT[:delete])
|
183
319
|
write(req)
|
184
320
|
generic_response unless multi?
|
185
321
|
end
|
@@ -190,46 +326,52 @@ module Dalli
|
|
190
326
|
generic_response
|
191
327
|
end
|
192
328
|
|
193
|
-
def
|
194
|
-
expiry = default ? ttl : 0xFFFFFFFF
|
329
|
+
def decr_incr(opcode, key, count, ttl, default)
|
330
|
+
expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
|
195
331
|
default ||= 0
|
196
332
|
(h, l) = split(count)
|
197
333
|
(dh, dl) = split(default)
|
198
|
-
req = [REQUEST, OPCODES[
|
334
|
+
req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
|
199
335
|
write(req)
|
200
336
|
body = generic_response
|
201
|
-
body ?
|
337
|
+
body ? body.unpack('Q>').first : body
|
338
|
+
end
|
339
|
+
|
340
|
+
def decr(key, count, ttl, default)
|
341
|
+
decr_incr :decr, key, count, ttl, default
|
202
342
|
end
|
203
343
|
|
204
344
|
def incr(key, count, ttl, default)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
345
|
+
decr_incr :incr, key, count, ttl, default
|
346
|
+
end
|
347
|
+
|
348
|
+
def write_append_prepend(opcode, key, value)
|
349
|
+
write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode])
|
350
|
+
end
|
351
|
+
|
352
|
+
def write_generic(bytes)
|
353
|
+
write(bytes)
|
354
|
+
generic_response
|
355
|
+
end
|
356
|
+
|
357
|
+
def write_noop
|
358
|
+
req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
210
359
|
write(req)
|
211
|
-
body = generic_response
|
212
|
-
body ? longlong(*body.unpack('NN')) : body
|
213
360
|
end
|
214
361
|
|
215
362
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
216
363
|
# We need to read all the responses at once.
|
217
364
|
def noop
|
218
|
-
|
219
|
-
write(req)
|
365
|
+
write_noop
|
220
366
|
multi_response
|
221
367
|
end
|
222
368
|
|
223
369
|
def append(key, value)
|
224
|
-
|
225
|
-
write(req)
|
226
|
-
generic_response
|
370
|
+
write_append_prepend :append, key, value
|
227
371
|
end
|
228
372
|
|
229
373
|
def prepend(key, value)
|
230
|
-
|
231
|
-
write(req)
|
232
|
-
generic_response
|
374
|
+
write_append_prepend :prepend, key, value
|
233
375
|
end
|
234
376
|
|
235
377
|
def stats(info='')
|
@@ -238,24 +380,29 @@ module Dalli
|
|
238
380
|
keyvalue_response
|
239
381
|
end
|
240
382
|
|
383
|
+
def reset_stats
|
384
|
+
write_generic [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
|
385
|
+
end
|
386
|
+
|
241
387
|
def cas(key)
|
242
388
|
req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
|
243
389
|
write(req)
|
244
|
-
|
390
|
+
data_cas_response
|
245
391
|
end
|
246
392
|
|
247
393
|
def version
|
248
|
-
|
249
|
-
write(req)
|
250
|
-
generic_response
|
394
|
+
write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
251
395
|
end
|
252
396
|
|
253
|
-
|
397
|
+
def touch(key, ttl)
|
398
|
+
ttl = sanitize_ttl(ttl)
|
399
|
+
write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
|
400
|
+
end
|
254
401
|
|
255
402
|
# http://www.hjp.at/zettel/m/memcached_flags.rxml
|
256
403
|
# Looks like most clients use bit 0 to indicate native language serialization
|
257
404
|
# and bit 1 to indicate gzip compression.
|
258
|
-
|
405
|
+
FLAG_SERIALIZED = 0x1
|
259
406
|
FLAG_COMPRESSED = 0x2
|
260
407
|
|
261
408
|
def serialize(key, value, options=nil)
|
@@ -263,7 +410,9 @@ module Dalli
|
|
263
410
|
value = unless options && options[:raw]
|
264
411
|
marshalled = true
|
265
412
|
begin
|
266
|
-
|
413
|
+
self.serializer.dump(value)
|
414
|
+
rescue Timeout::Error => e
|
415
|
+
raise e
|
267
416
|
rescue => ex
|
268
417
|
# Marshalling can throw several different types of generic Ruby exceptions.
|
269
418
|
# Convert to a specific exception so we can special case it higher up the stack.
|
@@ -275,31 +424,38 @@ module Dalli
|
|
275
424
|
value.to_s
|
276
425
|
end
|
277
426
|
compressed = false
|
278
|
-
if
|
279
|
-
|
427
|
+
set_compress_option = true if options && options[:compress]
|
428
|
+
if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size] &&
|
429
|
+
(!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size])
|
430
|
+
value = self.compressor.compress(value)
|
280
431
|
compressed = true
|
281
432
|
end
|
282
|
-
|
433
|
+
|
283
434
|
flags = 0
|
284
435
|
flags |= FLAG_COMPRESSED if compressed
|
285
|
-
flags |=
|
436
|
+
flags |= FLAG_SERIALIZED if marshalled
|
286
437
|
[value, flags]
|
287
438
|
end
|
288
439
|
|
289
440
|
def deserialize(value, flags)
|
290
|
-
value =
|
291
|
-
value =
|
441
|
+
value = self.compressor.decompress(value) if (flags & FLAG_COMPRESSED) != 0
|
442
|
+
value = self.serializer.load(value) if (flags & FLAG_SERIALIZED) != 0
|
292
443
|
value
|
293
|
-
rescue TypeError
|
294
|
-
raise
|
444
|
+
rescue TypeError
|
445
|
+
raise if $!.message !~ /needs to have method `_load'|exception class\/object expected|instance of IO needed|incompatible marshal file format/
|
446
|
+
raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
|
447
|
+
rescue ArgumentError
|
448
|
+
raise if $!.message !~ /undefined class|marshal data too short/
|
449
|
+
raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
|
450
|
+
rescue NameError
|
451
|
+
raise if $!.message !~ /uninitialized constant/
|
452
|
+
raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
|
295
453
|
rescue Zlib::Error
|
296
|
-
raise
|
454
|
+
raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
|
297
455
|
end
|
298
456
|
|
299
|
-
def
|
300
|
-
|
301
|
-
raise Dalli::NetworkError, 'No response' if !header
|
302
|
-
(extras, type, status, count, _, cas) = header.unpack(CAS_HEADER)
|
457
|
+
def data_cas_response
|
458
|
+
(extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
|
303
459
|
data = read(count) if count > 0
|
304
460
|
if status == 1
|
305
461
|
nil
|
@@ -315,15 +471,41 @@ module Dalli
|
|
315
471
|
|
316
472
|
CAS_HEADER = '@4CCnNNQ'
|
317
473
|
NORMAL_HEADER = '@4CCnN'
|
318
|
-
KV_HEADER = '@2n@6nN'
|
474
|
+
KV_HEADER = '@2n@6nN@16Q'
|
319
475
|
|
320
|
-
def
|
321
|
-
|
322
|
-
|
323
|
-
|
476
|
+
def guard_max_value(key, value)
|
477
|
+
if value.bytesize <= @options[:value_max_bytes]
|
478
|
+
yield
|
479
|
+
else
|
480
|
+
message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
|
481
|
+
raise Dalli::ValueOverMaxSize, message if @options[:error_when_over_max_size]
|
482
|
+
|
483
|
+
Dalli.logger.error "#{message} - this value may be truncated by memcached"
|
484
|
+
false
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
|
489
|
+
# > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
|
490
|
+
MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days
|
491
|
+
def sanitize_ttl(ttl)
|
492
|
+
ttl_as_i = ttl.to_i
|
493
|
+
return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
|
494
|
+
now = Time.now.to_i
|
495
|
+
return ttl_as_i if ttl_as_i > now # already a timestamp
|
496
|
+
Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
|
497
|
+
now + ttl_as_i
|
498
|
+
end
|
499
|
+
|
500
|
+
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
501
|
+
class NilObject; end
|
502
|
+
NOT_FOUND = NilObject.new
|
503
|
+
|
504
|
+
def generic_response(unpack=false, cache_nils=false)
|
505
|
+
(extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
|
324
506
|
data = read(count) if count > 0
|
325
507
|
if status == 1
|
326
|
-
nil
|
508
|
+
cache_nils ? NOT_FOUND : nil
|
327
509
|
elsif status == 2 || status == 5
|
328
510
|
false # Not stored, normal status for add operation
|
329
511
|
elsif status != 0
|
@@ -337,12 +519,24 @@ module Dalli
|
|
337
519
|
end
|
338
520
|
end
|
339
521
|
|
522
|
+
def cas_response
|
523
|
+
(_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
|
524
|
+
read(count) if count > 0 # this is potential data that we don't care about
|
525
|
+
if status == 1
|
526
|
+
nil
|
527
|
+
elsif status == 2 || status == 5
|
528
|
+
false # Not stored, normal status for add operation
|
529
|
+
elsif status != 0
|
530
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
531
|
+
else
|
532
|
+
cas
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
340
536
|
def keyvalue_response
|
341
537
|
hash = {}
|
342
|
-
|
343
|
-
|
344
|
-
raise Dalli::NetworkError, 'No response' if !header
|
345
|
-
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
538
|
+
while true
|
539
|
+
(key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
|
346
540
|
return hash if key_length == 0
|
347
541
|
key = read(key_length)
|
348
542
|
value = read(body_length - key_length) if body_length - key_length > 0
|
@@ -352,10 +546,8 @@ module Dalli
|
|
352
546
|
|
353
547
|
def multi_response
|
354
548
|
hash = {}
|
355
|
-
|
356
|
-
|
357
|
-
raise Dalli::NetworkError, 'No response' if !header
|
358
|
-
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
549
|
+
while true
|
550
|
+
(key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
|
359
551
|
return hash if key_length == 0
|
360
552
|
flags = read(4).unpack('N')[0]
|
361
553
|
key = read(key_length)
|
@@ -366,43 +558,48 @@ module Dalli
|
|
366
558
|
|
367
559
|
def write(bytes)
|
368
560
|
begin
|
369
|
-
@
|
370
|
-
|
371
|
-
|
372
|
-
|
561
|
+
@inprogress = true
|
562
|
+
result = @sock.write(bytes)
|
563
|
+
@inprogress = false
|
564
|
+
result
|
565
|
+
rescue SystemCallError, Timeout::Error => e
|
566
|
+
failure!(e)
|
373
567
|
end
|
374
568
|
end
|
375
569
|
|
376
570
|
def read(count)
|
377
571
|
begin
|
378
|
-
@
|
379
|
-
|
380
|
-
|
381
|
-
|
572
|
+
@inprogress = true
|
573
|
+
data = @sock.readfull(count)
|
574
|
+
@inprogress = false
|
575
|
+
data
|
576
|
+
rescue SystemCallError, Timeout::Error, EOFError => e
|
577
|
+
failure!(e)
|
382
578
|
end
|
383
579
|
end
|
384
580
|
|
581
|
+
def read_header
|
582
|
+
read(24) || raise(Dalli::NetworkError, 'No response')
|
583
|
+
end
|
584
|
+
|
385
585
|
def connect
|
386
|
-
Dalli.logger.debug { "Dalli::Server#connect #{
|
586
|
+
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
387
587
|
|
388
588
|
begin
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
raise Dalli::DalliError, "EM support not enabled, as em-synchrony is not installed." if not defined?(AsyncSocket)
|
393
|
-
@sock = AsyncSocket.open(hostname, port, :timeout => options[:socket_timeout])
|
589
|
+
@pid = Process.pid
|
590
|
+
if socket_type == :unix
|
591
|
+
@sock = KSocket::UNIX.open(hostname, self, options)
|
394
592
|
else
|
395
|
-
@sock = KSocket.open(hostname, port,
|
593
|
+
@sock = KSocket::TCP.open(hostname, port, self, options)
|
396
594
|
end
|
397
|
-
@version = version # trigger actual connect
|
398
595
|
sasl_authentication if need_auth?
|
596
|
+
@version = version # trigger actual connect
|
399
597
|
up!
|
400
598
|
rescue Dalli::DalliError # SASL auth failure
|
401
599
|
raise
|
402
|
-
rescue SystemCallError, Timeout::Error, EOFError, SocketError
|
600
|
+
rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
|
403
601
|
# SocketError = DNS resolution failure
|
404
|
-
failure!
|
405
|
-
retry
|
602
|
+
failure!(e)
|
406
603
|
end
|
407
604
|
end
|
408
605
|
|
@@ -410,13 +607,11 @@ module Dalli
|
|
410
607
|
[n >> 32, 0xFFFFFFFF & n]
|
411
608
|
end
|
412
609
|
|
413
|
-
def longlong(a, b)
|
414
|
-
(a << 32) | b
|
415
|
-
end
|
416
|
-
|
417
610
|
REQUEST = 0x80
|
418
611
|
RESPONSE = 0x81
|
419
612
|
|
613
|
+
# Response codes taken from:
|
614
|
+
# https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
|
420
615
|
RESPONSE_CODES = {
|
421
616
|
0 => 'No error',
|
422
617
|
1 => 'Key not found',
|
@@ -425,9 +620,16 @@ module Dalli
|
|
425
620
|
4 => 'Invalid arguments',
|
426
621
|
5 => 'Item not stored',
|
427
622
|
6 => 'Incr/decr on a non-numeric value',
|
623
|
+
7 => 'The vbucket belongs to another server',
|
624
|
+
8 => 'Authentication error',
|
625
|
+
9 => 'Authentication continue',
|
428
626
|
0x20 => 'Authentication required',
|
429
627
|
0x81 => 'Unknown command',
|
430
628
|
0x82 => 'Out of memory',
|
629
|
+
0x83 => 'Not supported',
|
630
|
+
0x84 => 'Internal error',
|
631
|
+
0x85 => 'Busy',
|
632
|
+
0x86 => 'Temporary failure'
|
431
633
|
}
|
432
634
|
|
433
635
|
OPCODES = {
|
@@ -454,6 +656,7 @@ module Dalli
|
|
454
656
|
:auth_negotiation => 0x20,
|
455
657
|
:auth_request => 0x21,
|
456
658
|
:auth_continue => 0x22,
|
659
|
+
:touch => 0x1C,
|
457
660
|
}
|
458
661
|
|
459
662
|
HEADER = "CCnCCnNNQ"
|
@@ -474,6 +677,7 @@ module Dalli
|
|
474
677
|
:prepend => 'a*a*',
|
475
678
|
:auth_request => 'a*a*',
|
476
679
|
:auth_continue => 'a*a*',
|
680
|
+
:touch => 'Na*',
|
477
681
|
}
|
478
682
|
FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
|
479
683
|
|
@@ -500,11 +704,10 @@ module Dalli
|
|
500
704
|
# negotiate
|
501
705
|
req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
502
706
|
write(req)
|
503
|
-
|
504
|
-
|
505
|
-
(extras, type, status, count) = header.unpack(NORMAL_HEADER)
|
707
|
+
|
708
|
+
(extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
|
506
709
|
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
507
|
-
content = read(count)
|
710
|
+
content = read(count).gsub(/\u0000/, ' ')
|
508
711
|
return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
|
509
712
|
mechanisms = content.split(' ')
|
510
713
|
raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
|
@@ -515,9 +718,7 @@ module Dalli
|
|
515
718
|
req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
|
516
719
|
write(req)
|
517
720
|
|
518
|
-
|
519
|
-
raise Dalli::NetworkError, 'No response' if !header
|
520
|
-
(extras, type, status, count) = header.unpack(NORMAL_HEADER)
|
721
|
+
(extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
|
521
722
|
raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
|
522
723
|
content = read(count)
|
523
724
|
return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
|
@@ -527,5 +728,25 @@ module Dalli
|
|
527
728
|
# (step, msg) = sasl.receive('challenge', content)
|
528
729
|
# raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
|
529
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 =~ /\A\//
|
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
|
+
return hostnam, por, weigh, socket_type
|
750
|
+
end
|
530
751
|
end
|
531
752
|
end
|