dalli 0.9.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.
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/Performance.md +24 -0
- data/README.md +75 -0
- data/Rakefile +7 -0
- data/dalli.gemspec +30 -0
- data/lib/active_support/cache/dalli_store.rb +177 -0
- data/lib/dalli.rb +25 -0
- data/lib/dalli/client.rb +122 -0
- data/lib/dalli/options.rb +70 -0
- data/lib/dalli/ring.rb +99 -0
- data/lib/dalli/server.rb +303 -0
- data/lib/dalli/version.rb +3 -0
- data/test/helper.rb +12 -0
- data/test/memcached_mock.rb +45 -0
- data/test/test_active_support.rb +75 -0
- data/test/test_benchmark.rb +118 -0
- data/test/test_dalli.rb +97 -0
- data/test/test_network.rb +59 -0
- metadata +130 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
|
5
|
+
# Auto-marshal all values in/out of memcached.
|
6
|
+
# Otherwise, Dalli will just use to_s on all values.
|
7
|
+
#
|
8
|
+
# Dalli::Client.extend(Dalli::Marshal)
|
9
|
+
#
|
10
|
+
module Marshal
|
11
|
+
def serialize(value)
|
12
|
+
::Marshal.dump(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(value)
|
16
|
+
begin
|
17
|
+
::Marshal.load(value)
|
18
|
+
rescue TypeError
|
19
|
+
raise Dalli::DalliError, "Invalid marshalled data in memcached, this happens if you switch the :marshal option and still have old data in memcached: #{value}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def append(key, value)
|
24
|
+
raise Dalli::DalliError, "Marshalling and append do not work together"
|
25
|
+
end
|
26
|
+
|
27
|
+
def prepend(key, value)
|
28
|
+
raise Dalli::DalliError, "Marshalling and prepend do not work together"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Make Dalli threadsafe by using a lock around all
|
33
|
+
# public server methods.
|
34
|
+
#
|
35
|
+
# Dalli::Server.extend(Dalli::Threadsafe)
|
36
|
+
#
|
37
|
+
module Threadsafe
|
38
|
+
def request(op, *args)
|
39
|
+
lock.synchronize do
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def alive?
|
45
|
+
lock.synchronize do
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def close
|
51
|
+
lock.synchronize do
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def lock!
|
57
|
+
lock.mon_enter
|
58
|
+
end
|
59
|
+
|
60
|
+
def unlock!
|
61
|
+
lock.mon_exit
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def lock
|
66
|
+
@lock ||= Monitor.new
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
data/lib/dalli/ring.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Dalli
|
5
|
+
class Ring
|
6
|
+
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
7
|
+
|
8
|
+
attr_accessor :servers, :continuum
|
9
|
+
|
10
|
+
def initialize(servers)
|
11
|
+
@servers = servers
|
12
|
+
if servers.size > 1
|
13
|
+
total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
|
14
|
+
continuum = []
|
15
|
+
servers.each do |server|
|
16
|
+
entry_count_for(server, servers.size, total_weight).times do |idx|
|
17
|
+
hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
|
18
|
+
value = Integer("0x#{hash[0..7]}")
|
19
|
+
continuum << Dalli::Ring::Entry.new(value, server)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
continuum.sort { |a, b| a.value <=> b.value }
|
23
|
+
@continuum = continuum
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def server_for_key(key)
|
28
|
+
return @servers.first unless @continuum
|
29
|
+
|
30
|
+
hkey = hash_for(key)
|
31
|
+
|
32
|
+
20.times do |try|
|
33
|
+
entryidx = self.class.binary_search(@continuum, hkey)
|
34
|
+
server = @continuum[entryidx].server
|
35
|
+
return server if server.alive?
|
36
|
+
hkey = hash_for("#{try}#{key}")
|
37
|
+
end
|
38
|
+
|
39
|
+
raise Dalli::NetworkError, "No servers available"
|
40
|
+
end
|
41
|
+
|
42
|
+
def threadsafe!
|
43
|
+
@servers.each do |s|
|
44
|
+
s.extend(Dalli::Threadsafe)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def lock
|
49
|
+
@servers.each { |s| s.lock! }
|
50
|
+
begin
|
51
|
+
return yield
|
52
|
+
ensure
|
53
|
+
@servers.each { |s| s.unlock! }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def hash_for(key)
|
60
|
+
Zlib.crc32(key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def entry_count_for(server, total_servers, total_weight)
|
64
|
+
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
65
|
+
end
|
66
|
+
|
67
|
+
# Find the closest index in the Ring with value <= the given value
|
68
|
+
def self.binary_search(ary, value)
|
69
|
+
upper = ary.size - 1
|
70
|
+
lower = 0
|
71
|
+
idx = 0
|
72
|
+
|
73
|
+
while (lower <= upper) do
|
74
|
+
idx = (lower + upper) / 2
|
75
|
+
comp = ary[idx].value <=> value
|
76
|
+
|
77
|
+
if comp == 0
|
78
|
+
return idx
|
79
|
+
elsif comp > 0
|
80
|
+
upper = idx - 1
|
81
|
+
else
|
82
|
+
lower = idx + 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
return upper
|
86
|
+
end
|
87
|
+
|
88
|
+
class Entry
|
89
|
+
attr_reader :value
|
90
|
+
attr_reader :server
|
91
|
+
|
92
|
+
def initialize(val, srv)
|
93
|
+
@value = val
|
94
|
+
@server = srv
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/dalli/server.rb
ADDED
@@ -0,0 +1,303 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
class Server
|
5
|
+
attr_accessor :hostname
|
6
|
+
attr_accessor :port
|
7
|
+
attr_accessor :weight
|
8
|
+
|
9
|
+
def initialize(attribs)
|
10
|
+
(@hostname, @port, @weight) = attribs.split(':')
|
11
|
+
@port ||= 11211
|
12
|
+
@port = Integer(@port)
|
13
|
+
@weight ||= 1
|
14
|
+
@weight = Integer(@weight)
|
15
|
+
connection
|
16
|
+
Dalli.logger.debug { "#{@hostname}:#{@port} running memcached v#{request(:version)}" }
|
17
|
+
end
|
18
|
+
|
19
|
+
def request(op, *args)
|
20
|
+
begin
|
21
|
+
send(op, *args)
|
22
|
+
rescue Dalli::NetworkError
|
23
|
+
raise
|
24
|
+
rescue Dalli::DalliError
|
25
|
+
raise
|
26
|
+
rescue Exception => ex
|
27
|
+
puts "Unexpected exception: #{ex.class.name}: #{ex.message}"
|
28
|
+
puts ex.backtrace.join("\n")
|
29
|
+
down!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def alive?
|
34
|
+
@sock && !@sock.closed?
|
35
|
+
end
|
36
|
+
|
37
|
+
def close
|
38
|
+
(@sock.close rescue nil; @sock = nil) if @sock
|
39
|
+
end
|
40
|
+
|
41
|
+
def lock!
|
42
|
+
end
|
43
|
+
|
44
|
+
def unlock!
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def down!
|
50
|
+
close
|
51
|
+
@down_at = Time.now.to_i
|
52
|
+
@msg = $!.message
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
ONE_MB = 1024 * 1024
|
57
|
+
|
58
|
+
def get(key)
|
59
|
+
req = [REQUEST, OPCODES[:get], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:get])
|
60
|
+
write(req)
|
61
|
+
generic_response
|
62
|
+
end
|
63
|
+
|
64
|
+
def getkq(key)
|
65
|
+
req = [REQUEST, OPCODES[:getkq], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:getkq])
|
66
|
+
write(req)
|
67
|
+
end
|
68
|
+
|
69
|
+
def set(key, value, ttl=0)
|
70
|
+
raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
|
71
|
+
|
72
|
+
req = [REQUEST, OPCODES[:set], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:set])
|
73
|
+
write(req)
|
74
|
+
generic_response
|
75
|
+
end
|
76
|
+
|
77
|
+
def flush(ttl)
|
78
|
+
req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
|
79
|
+
write(req)
|
80
|
+
generic_response
|
81
|
+
end
|
82
|
+
|
83
|
+
def add(key, value, ttl)
|
84
|
+
raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
|
85
|
+
|
86
|
+
req = [REQUEST, OPCODES[:add], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:add])
|
87
|
+
write(req)
|
88
|
+
generic_response
|
89
|
+
end
|
90
|
+
|
91
|
+
def append(key, value)
|
92
|
+
req = [REQUEST, OPCODES[:append], key.size, 0, 0, 0, value.size + key.size, 0, 0, key, value].pack(FORMAT[:append])
|
93
|
+
write(req)
|
94
|
+
generic_response
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete(key)
|
98
|
+
req = [REQUEST, OPCODES[:delete], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:delete])
|
99
|
+
write(req)
|
100
|
+
generic_response
|
101
|
+
end
|
102
|
+
|
103
|
+
def decr(key, count)
|
104
|
+
raise NotImplementedError
|
105
|
+
end
|
106
|
+
|
107
|
+
def incr(key, count)
|
108
|
+
raise NotImplementedError
|
109
|
+
end
|
110
|
+
|
111
|
+
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
112
|
+
# We need to read all the responses at once.
|
113
|
+
def noop
|
114
|
+
req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
115
|
+
write(req)
|
116
|
+
multi_response
|
117
|
+
end
|
118
|
+
|
119
|
+
def prepend(key, value)
|
120
|
+
req = [REQUEST, OPCODES[:prepend], key.size, 0, 0, 0, value.size + key.size, 0, 0, key, value].pack(FORMAT[:prepend])
|
121
|
+
write(req)
|
122
|
+
generic_response
|
123
|
+
end
|
124
|
+
|
125
|
+
def replace(key, value, ttl)
|
126
|
+
req = [REQUEST, OPCODES[:replace], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:replace])
|
127
|
+
write(req)
|
128
|
+
generic_response
|
129
|
+
end
|
130
|
+
|
131
|
+
def version
|
132
|
+
req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
|
133
|
+
write(req)
|
134
|
+
generic_response
|
135
|
+
end
|
136
|
+
|
137
|
+
def stats(info='')
|
138
|
+
req = [REQUEST, OPCODES[:stat], info.size, 0, 0, 0, info.size, 0, 0, info].pack(FORMAT[:stat])
|
139
|
+
write(req)
|
140
|
+
keyvalue_response
|
141
|
+
end
|
142
|
+
|
143
|
+
def generic_response
|
144
|
+
header = read(24)
|
145
|
+
raise Dalli::NetworkError, 'No response' if !header
|
146
|
+
(extras, status, count) = header.unpack(NORMAL_HEADER)
|
147
|
+
data = read(count) if count > 0
|
148
|
+
if status == 1
|
149
|
+
nil
|
150
|
+
elsif status != 0
|
151
|
+
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
152
|
+
elsif data
|
153
|
+
extras != 0 ? data[extras..-1] : data
|
154
|
+
else
|
155
|
+
true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def keyvalue_response
|
160
|
+
hash = {}
|
161
|
+
loop do
|
162
|
+
header = read(24)
|
163
|
+
raise Dalli::NetworkError, 'No response' if !header
|
164
|
+
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
165
|
+
return hash if key_length == 0
|
166
|
+
key = read(key_length)
|
167
|
+
value = read(body_length - key_length) if body_length - key_length > 0
|
168
|
+
hash[key] = value
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def multi_response
|
173
|
+
hash = {}
|
174
|
+
loop do
|
175
|
+
header = read(24)
|
176
|
+
raise Dalli::NetworkError, 'No response' if !header
|
177
|
+
(key_length, status, body_length) = header.unpack(KV_HEADER)
|
178
|
+
return hash if key_length == 0
|
179
|
+
read(4)
|
180
|
+
key = read(key_length)
|
181
|
+
value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
|
182
|
+
hash[key] = value
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
TIMEOUT = 0.5
|
187
|
+
|
188
|
+
def connection
|
189
|
+
@sock ||= begin
|
190
|
+
if @down_at && @down_at == Time.now.to_i
|
191
|
+
raise Dalli::NetworkError, "#{self.hostname}:#{self.port} is currently down: #{@msg}"
|
192
|
+
end
|
193
|
+
|
194
|
+
# All this ugly code to ensure proper Socket connect timeout
|
195
|
+
addr = Socket.getaddrinfo(self.hostname, nil)
|
196
|
+
sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
|
197
|
+
begin
|
198
|
+
sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
|
199
|
+
rescue Errno::EINPROGRESS
|
200
|
+
resp = IO.select(nil, [sock], nil, TIMEOUT)
|
201
|
+
begin
|
202
|
+
sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
|
203
|
+
rescue Errno::EISCONN
|
204
|
+
;
|
205
|
+
rescue
|
206
|
+
raise Dalli::NetworkError, "#{self.hostname}:#{self.port} is currently down: #{$!.message}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
# end ugly code
|
210
|
+
|
211
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
212
|
+
sock
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def write(bytes)
|
217
|
+
begin
|
218
|
+
connection.write(bytes)
|
219
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF
|
220
|
+
down!
|
221
|
+
raise Dalli::NetworkError, $!.class.name
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def read(count)
|
226
|
+
begin
|
227
|
+
value = ''
|
228
|
+
begin
|
229
|
+
loop do
|
230
|
+
value << connection.sysread(count)
|
231
|
+
break if value.size == count
|
232
|
+
end
|
233
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
234
|
+
if IO.select([connection], nil, nil, TIMEOUT)
|
235
|
+
retry
|
236
|
+
else
|
237
|
+
raise Timeout::Error, "IO timeout"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
raise Errno::EINVAL, "Not enough data to fulfill read request: #{value.inspect}" if value.size != count
|
241
|
+
value
|
242
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, Timeout::Error, EOFError
|
243
|
+
down!
|
244
|
+
raise Dalli::NetworkError, "#{$!.class.name}: #{$!.message}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
NORMAL_HEADER = '@4vnN'
|
249
|
+
KV_HEADER = '@2n@6nN'
|
250
|
+
|
251
|
+
REQUEST = 0x80
|
252
|
+
RESPONSE = 0x81
|
253
|
+
|
254
|
+
RESPONSE_CODES = {
|
255
|
+
0 => 'No error',
|
256
|
+
1 => 'Key not found',
|
257
|
+
2 => 'Key exists',
|
258
|
+
3 => 'Value too large',
|
259
|
+
4 => 'Invalid arguments',
|
260
|
+
5 => 'Item not stored',
|
261
|
+
6 => 'Incr/decr on a non-numeric value',
|
262
|
+
0x81 => 'Unknown command',
|
263
|
+
0x82 => 'Out of memory',
|
264
|
+
}
|
265
|
+
|
266
|
+
OPCODES = {
|
267
|
+
:get => 0x00,
|
268
|
+
:set => 0x01,
|
269
|
+
:add => 0x02,
|
270
|
+
:replace => 0x03,
|
271
|
+
:delete => 0x04,
|
272
|
+
:incr => 0x05,
|
273
|
+
:decr => 0x06,
|
274
|
+
:flush => 0x08,
|
275
|
+
:noop => 0x0A,
|
276
|
+
:version => 0x0B,
|
277
|
+
:getkq => 0x0D,
|
278
|
+
:append => 0x0E,
|
279
|
+
:prepend => 0x0F,
|
280
|
+
:stat => 0x10,
|
281
|
+
}
|
282
|
+
|
283
|
+
HEADER = "CCnCCnNNQ"
|
284
|
+
OP_FORMAT = {
|
285
|
+
:get => 'a*',
|
286
|
+
:set => 'NNa*a*',
|
287
|
+
:add => 'NNa*a*',
|
288
|
+
:replace => 'NNa*a*',
|
289
|
+
:delete => 'a*',
|
290
|
+
:incr => 'NNNNNa*',
|
291
|
+
:decr => 'NNNNNa*',
|
292
|
+
:flush => 'N',
|
293
|
+
:noop => '',
|
294
|
+
:getkq => 'a*',
|
295
|
+
:version => '',
|
296
|
+
:stat => 'a*',
|
297
|
+
:append => 'a*a*',
|
298
|
+
:prepend => 'a*a*',
|
299
|
+
}
|
300
|
+
FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
|
301
|
+
|
302
|
+
end
|
303
|
+
end
|