mob-dalli 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +11 -0
- data/History.md +226 -0
- data/LICENSE +20 -0
- data/Performance.md +85 -0
- data/README.md +182 -0
- data/Rakefile +39 -0
- data/Upgrade.md +45 -0
- data/lib/action_dispatch/middleware/session/dalli_store.rb +76 -0
- data/lib/active_support/cache/dalli_store.rb +184 -0
- data/lib/active_support/cache/dalli_store23.rb +172 -0
- data/lib/dalli/client.rb +291 -0
- data/lib/dalli/compatibility.rb +52 -0
- data/lib/dalli/memcache-client.rb +1 -0
- data/lib/dalli/options.rb +46 -0
- data/lib/dalli/ring.rb +105 -0
- data/lib/dalli/server.rb +532 -0
- data/lib/dalli/socket.rb +170 -0
- data/lib/dalli/version.rb +3 -0
- data/lib/mob-dalli.rb +47 -0
- data/lib/rack/session/dalli.rb +52 -0
- data/mob-dalli.gemspec +31 -0
- data/test/abstract_unit.rb +282 -0
- data/test/benchmark_test.rb +170 -0
- data/test/helper.rb +39 -0
- data/test/memcached_mock.rb +126 -0
- data/test/test_active_support.rb +201 -0
- data/test/test_compatibility.rb +33 -0
- data/test/test_dalli.rb +450 -0
- data/test/test_encoding.rb +51 -0
- data/test/test_failover.rb +107 -0
- data/test/test_network.rb +54 -0
- data/test/test_ring.rb +85 -0
- data/test/test_sasl.rb +83 -0
- data/test/test_session_store.rb +225 -0
- data/test/test_synchrony.rb +175 -0
- metadata +172 -0
data/lib/dalli/client.rb
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
# encoding: ascii
|
2
|
+
module Dalli
|
3
|
+
class Client
|
4
|
+
|
5
|
+
##
|
6
|
+
# Dalli::Client is the main class which developers will use to interact with
|
7
|
+
# the memcached server. Usage:
|
8
|
+
#
|
9
|
+
# Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
|
10
|
+
# :threadsafe => true, :failover => true, :expires_in => 300)
|
11
|
+
#
|
12
|
+
# servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
|
13
|
+
# Both weight and port are optional. If you pass in nil, Dalli will default to 'localhost:11211'.
|
14
|
+
# Note that the <tt>MEMCACHE_SERVERS</tt> environment variable will override the servers parameter for use
|
15
|
+
# in managed environments like Heroku.
|
16
|
+
#
|
17
|
+
# You can also provide a Unix socket as an argument, for example:
|
18
|
+
#
|
19
|
+
# Dalli::Client.new("/tmp/memcached.sock")
|
20
|
+
#
|
21
|
+
# Initial testing shows that Unix sockets are about twice as fast as TCP sockets
|
22
|
+
# but Unix sockets only work on localhost.
|
23
|
+
#
|
24
|
+
# Options:
|
25
|
+
# - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
|
26
|
+
# - :nonascii - allow the use of nonascii key names. Default: false.
|
27
|
+
# - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
|
28
|
+
# - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever
|
29
|
+
# - :compression - defaults to false, if true Dalli will compress values larger than 100 bytes before
|
30
|
+
# sending them to memcached.
|
31
|
+
# - :async - assume its running inside the EM reactor. Requires em-synchrony to be installed. Default: false.
|
32
|
+
#
|
33
|
+
def initialize(servers=nil, options={})
|
34
|
+
@servers = env_servers || servers || 'localhost:11211'
|
35
|
+
@options = { :expires_in => 0 }.merge(options)
|
36
|
+
self.extend(Dalli::Client::MemcacheClientCompatibility) if Dalli::Client.compatibility_mode
|
37
|
+
@ring = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Turn on compatibility mode, which mixes in methods in memcache_client_compatibility.rb
|
42
|
+
# This value is set to true in memcache-client.rb.
|
43
|
+
def self.compatibility_mode
|
44
|
+
@compatibility_mode ||= false
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.compatibility_mode=(compatibility_mode)
|
48
|
+
require 'dalli/compatibility'
|
49
|
+
@compatibility_mode = compatibility_mode
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# The standard memcached instruction set
|
54
|
+
#
|
55
|
+
|
56
|
+
##
|
57
|
+
# Turn on quiet aka noreply support.
|
58
|
+
# All relevant operations within this block will be effectively
|
59
|
+
# pipelined as Dalli will use 'quiet' operations where possible.
|
60
|
+
# Currently supports the set, add, replace and delete operations.
|
61
|
+
def multi
|
62
|
+
old, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true
|
63
|
+
yield
|
64
|
+
ensure
|
65
|
+
Thread.current[:dalli_multi] = old
|
66
|
+
end
|
67
|
+
|
68
|
+
def get(key, options=nil)
|
69
|
+
resp = perform(:get, key)
|
70
|
+
(!resp || resp == 'Not found') ? nil : resp
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Fetch multiple keys efficiently.
|
75
|
+
# Returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
76
|
+
def get_multi(*keys)
|
77
|
+
return {} if keys.empty?
|
78
|
+
options = nil
|
79
|
+
options = keys.pop if keys.last.is_a?(Hash) || keys.last.nil?
|
80
|
+
ring.lock do
|
81
|
+
keys.flatten.each do |key|
|
82
|
+
begin
|
83
|
+
perform(:getkq, key)
|
84
|
+
rescue DalliError, NetworkError => e
|
85
|
+
Dalli.logger.debug { e.message }
|
86
|
+
Dalli.logger.debug { "unable to get key #{key}" }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
values = {}
|
91
|
+
ring.servers.each do |server|
|
92
|
+
next unless server.alive?
|
93
|
+
begin
|
94
|
+
server.request(:noop).each_pair do |key, value|
|
95
|
+
values[key_without_namespace(key)] = value
|
96
|
+
end
|
97
|
+
rescue DalliError, NetworkError => e
|
98
|
+
Dalli.logger.debug { e.message }
|
99
|
+
Dalli.logger.debug { "results from this server will be missing" }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
values
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def fetch(key, ttl=nil, options=nil)
|
107
|
+
ttl ||= @options[:expires_in]
|
108
|
+
val = get(key, options)
|
109
|
+
if val.nil? && block_given?
|
110
|
+
val = yield
|
111
|
+
add(key, val, ttl, options)
|
112
|
+
end
|
113
|
+
val
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# compare and swap values using optimistic locking.
|
118
|
+
# Fetch the existing value for key.
|
119
|
+
# If it exists, yield the value to the block.
|
120
|
+
# Add the block's return value as the new value for the key.
|
121
|
+
# Add will fail if someone else changed the value.
|
122
|
+
#
|
123
|
+
# Returns:
|
124
|
+
# - nil if the key did not exist.
|
125
|
+
# - false if the value was changed by someone else.
|
126
|
+
# - true if the value was successfully updated.
|
127
|
+
def cas(key, ttl=nil, options=nil, &block)
|
128
|
+
ttl ||= @options[:expires_in]
|
129
|
+
(value, cas) = perform(:cas, key)
|
130
|
+
value = (!value || value == 'Not found') ? nil : value
|
131
|
+
if value
|
132
|
+
newvalue = block.call(value)
|
133
|
+
perform(:set, key, newvalue, ttl, cas, options)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def set(key, value, ttl=nil, options=nil)
|
138
|
+
raise "Invalid API usage, please require 'dalli/memcache-client' for compatibility, see Upgrade.md" if options == true
|
139
|
+
ttl ||= @options[:expires_in]
|
140
|
+
perform(:set, key, value, ttl, 0, options)
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Conditionally add a key/value pair, if the key does not already exist
|
145
|
+
# on the server. Returns true if the operation succeeded.
|
146
|
+
def add(key, value, ttl=nil, options=nil)
|
147
|
+
ttl ||= @options[:expires_in]
|
148
|
+
perform(:add, key, value, ttl, options)
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Conditionally add a key/value pair, only if the key already exists
|
153
|
+
# on the server. Returns true if the operation succeeded.
|
154
|
+
def replace(key, value, ttl=nil, options=nil)
|
155
|
+
ttl ||= @options[:expires_in]
|
156
|
+
perform(:replace, key, value, ttl, options)
|
157
|
+
end
|
158
|
+
|
159
|
+
def delete(key)
|
160
|
+
perform(:delete, key)
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Append value to the value already stored on the server for 'key'.
|
165
|
+
# Appending only works for values stored with :raw => true.
|
166
|
+
def append(key, value)
|
167
|
+
perform(:append, key, value.to_s)
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Prepend value to the value already stored on the server for 'key'.
|
172
|
+
# Prepending only works for values stored with :raw => true.
|
173
|
+
def prepend(key, value)
|
174
|
+
perform(:prepend, key, value.to_s)
|
175
|
+
end
|
176
|
+
|
177
|
+
def flush(delay=0)
|
178
|
+
time = -delay
|
179
|
+
ring.servers.map { |s| s.request(:flush, time += delay) }
|
180
|
+
end
|
181
|
+
|
182
|
+
# deprecated, please use #flush.
|
183
|
+
alias_method :flush_all, :flush
|
184
|
+
|
185
|
+
##
|
186
|
+
# Incr adds the given amount to the counter on the memcached server.
|
187
|
+
# Amt must be a positive value.
|
188
|
+
#
|
189
|
+
# If default is nil, the counter must already exist or the operation
|
190
|
+
# will fail and will return nil. Otherwise this method will return
|
191
|
+
# the new value for the counter.
|
192
|
+
#
|
193
|
+
# Note that the ttl will only apply if the counter does not already
|
194
|
+
# exist. To increase an existing counter and update its TTL, use
|
195
|
+
# #cas.
|
196
|
+
def incr(key, amt=1, ttl=nil, default=nil)
|
197
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
198
|
+
ttl ||= @options[:expires_in]
|
199
|
+
perform(:incr, key, amt, ttl, default)
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Decr subtracts the given amount from the counter on the memcached server.
|
204
|
+
# Amt must be a positive value.
|
205
|
+
#
|
206
|
+
# memcached counters are unsigned and cannot hold negative values. Calling
|
207
|
+
# decr on a counter which is 0 will just return 0.
|
208
|
+
#
|
209
|
+
# If default is nil, the counter must already exist or the operation
|
210
|
+
# will fail and will return nil. Otherwise this method will return
|
211
|
+
# the new value for the counter.
|
212
|
+
#
|
213
|
+
# Note that the ttl will only apply if the counter does not already
|
214
|
+
# exist. To decrease an existing counter and update its TTL, use
|
215
|
+
# #cas.
|
216
|
+
def decr(key, amt=1, ttl=nil, default=nil)
|
217
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
218
|
+
ttl ||= @options[:expires_in]
|
219
|
+
perform(:decr, key, amt, ttl, default)
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Collect the stats for each server.
|
224
|
+
# Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
|
225
|
+
def stats
|
226
|
+
values = {}
|
227
|
+
ring.servers.each do |server|
|
228
|
+
values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats) : nil
|
229
|
+
end
|
230
|
+
values
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# Close our connection to each server.
|
235
|
+
# If you perform another operation after this, the connections will be re-established.
|
236
|
+
def close
|
237
|
+
if @ring
|
238
|
+
@ring.servers.each { |s| s.close }
|
239
|
+
@ring = nil
|
240
|
+
end
|
241
|
+
end
|
242
|
+
alias_method :reset, :close
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def ring
|
247
|
+
@ring ||= Dalli::Ring.new(
|
248
|
+
Array(@servers).map do |s|
|
249
|
+
Dalli::Server.new(s, @options)
|
250
|
+
end, @options
|
251
|
+
)
|
252
|
+
end
|
253
|
+
|
254
|
+
def env_servers
|
255
|
+
ENV['MEMCACHE_SERVERS'] ? ENV['MEMCACHE_SERVERS'].split(',') : nil
|
256
|
+
end
|
257
|
+
|
258
|
+
# Chokepoint method for instrumentation
|
259
|
+
def perform(op, key, *args)
|
260
|
+
key = key.to_s
|
261
|
+
validate_key(key)
|
262
|
+
key = key_with_namespace(key)
|
263
|
+
begin
|
264
|
+
server = ring.server_for_key(key)
|
265
|
+
server.request(op, key, *args)
|
266
|
+
rescue NetworkError => e
|
267
|
+
Dalli.logger.debug { e.message }
|
268
|
+
Dalli.logger.debug { "retrying request with new server" }
|
269
|
+
retry
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def validate_key(key)
|
274
|
+
unless !!@options[:nonascii] === true
|
275
|
+
raise ArgumentError, "illegal character in key #{key}" if key.respond_to?(:ascii_only?) && !key.ascii_only?
|
276
|
+
raise ArgumentError, "illegal character in key #{key}" if key =~ /\s/
|
277
|
+
raise ArgumentError, "illegal character in key #{key}" if key =~ /[\x00-\x20\x80-\xFF]/
|
278
|
+
end
|
279
|
+
raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
|
280
|
+
raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
|
281
|
+
end
|
282
|
+
|
283
|
+
def key_with_namespace(key)
|
284
|
+
@options[:namespace] ? "#{@options[:namespace]}:#{key}" : key
|
285
|
+
end
|
286
|
+
|
287
|
+
def key_without_namespace(key)
|
288
|
+
@options[:namespace] ? key.gsub(%r(\A#{@options[:namespace]}:), '') : key
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Dalli::Client
|
2
|
+
|
3
|
+
module MemcacheClientCompatibility
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
Dalli.logger.error("Starting Dalli in memcache-client compatibility mode")
|
7
|
+
super(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(key, value, ttl = nil, options = nil)
|
11
|
+
if options == true || options == false
|
12
|
+
Dalli.logger.error("Dalli: please use set(key, value, ttl, :raw => boolean): #{caller[0]}")
|
13
|
+
options = { :raw => options }
|
14
|
+
end
|
15
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(key, value, ttl = nil, options = nil)
|
20
|
+
if options == true || options == false
|
21
|
+
Dalli.logger.error("Dalli: please use add(key, value, ttl, :raw => boolean): #{caller[0]}")
|
22
|
+
options = { :raw => options }
|
23
|
+
end
|
24
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
def replace(key, value, ttl = nil, options = nil)
|
28
|
+
if options == true || options == false
|
29
|
+
Dalli.logger.error("Dalli: please use replace(key, value, ttl, :raw => boolean): #{caller[0]}")
|
30
|
+
options = { :raw => options }
|
31
|
+
end
|
32
|
+
super(key, value, ttl, options) ? "STORED\r\n" : "NOT_STORED\r\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Dalli does not unmarshall data that does not have the marshalled flag set so we need
|
36
|
+
# to unmarshall manually any marshalled data originally put in memcached by memcache-client.
|
37
|
+
# Peek at the data and see if it looks marshalled.
|
38
|
+
def get(key, options = nil)
|
39
|
+
value = super(key, options)
|
40
|
+
if value && value.is_a?(String) && !options && value.size > 2 &&
|
41
|
+
(bytes = value.unpack('cc')) && bytes[0] == 4 && bytes[1] == 8
|
42
|
+
return Marshal.load(value) rescue value
|
43
|
+
end
|
44
|
+
value
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key)
|
48
|
+
super(key) ? "DELETED\r\n" : "NOT_DELETED\r\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dalli::Client.compatibility_mode = true
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'monitor'
|
3
|
+
|
4
|
+
module Dalli
|
5
|
+
|
6
|
+
# Make Dalli threadsafe by using a lock around all
|
7
|
+
# public server methods.
|
8
|
+
#
|
9
|
+
# Dalli::Server.extend(Dalli::Threadsafe)
|
10
|
+
#
|
11
|
+
module Threadsafe
|
12
|
+
def self.extended(obj)
|
13
|
+
obj.init_threadsafe
|
14
|
+
end
|
15
|
+
|
16
|
+
def request(op, *args)
|
17
|
+
@lock.synchronize do
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def alive?
|
23
|
+
@lock.synchronize do
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
@lock.synchronize do
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def lock!
|
35
|
+
@lock.mon_enter
|
36
|
+
end
|
37
|
+
|
38
|
+
def unlock!
|
39
|
+
@lock.mon_exit
|
40
|
+
end
|
41
|
+
|
42
|
+
def init_threadsafe
|
43
|
+
@lock = Monitor.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/dalli/ring.rb
ADDED
@@ -0,0 +1,105 @@
|
|
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, options)
|
11
|
+
@servers = servers
|
12
|
+
@continuum = nil
|
13
|
+
if servers.size > 1
|
14
|
+
total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
|
15
|
+
continuum = []
|
16
|
+
servers.each do |server|
|
17
|
+
entry_count_for(server, servers.size, total_weight).times do |idx|
|
18
|
+
hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
|
19
|
+
value = Integer("0x#{hash[0..7]}")
|
20
|
+
continuum << Dalli::Ring::Entry.new(value, server)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
@continuum = continuum.sort { |a, b| a.value <=> b.value }
|
24
|
+
end
|
25
|
+
|
26
|
+
threadsafe! unless options[:threadsafe] == false
|
27
|
+
@failover = options[:failover] != false
|
28
|
+
end
|
29
|
+
|
30
|
+
def server_for_key(key)
|
31
|
+
if @continuum
|
32
|
+
hkey = hash_for(key)
|
33
|
+
20.times do |try|
|
34
|
+
entryidx = self.class.binary_search(@continuum, hkey)
|
35
|
+
server = @continuum[entryidx].server
|
36
|
+
return server if server.alive?
|
37
|
+
break unless @failover
|
38
|
+
hkey = hash_for("#{try}#{key}")
|
39
|
+
end
|
40
|
+
else
|
41
|
+
server = @servers.first
|
42
|
+
return server if server && server.alive?
|
43
|
+
end
|
44
|
+
|
45
|
+
raise Dalli::RingError, "No server available"
|
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 threadsafe!
|
60
|
+
@servers.each do |s|
|
61
|
+
s.extend(Dalli::Threadsafe)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def hash_for(key)
|
66
|
+
Zlib.crc32(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def entry_count_for(server, total_servers, total_weight)
|
70
|
+
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
71
|
+
end
|
72
|
+
|
73
|
+
# Find the closest index in the Ring with value <= the given value
|
74
|
+
def self.binary_search(ary, value)
|
75
|
+
upper = ary.size - 1
|
76
|
+
lower = 0
|
77
|
+
idx = 0
|
78
|
+
|
79
|
+
while (lower <= upper) do
|
80
|
+
idx = (lower + upper) / 2
|
81
|
+
comp = ary[idx].value <=> value
|
82
|
+
|
83
|
+
if comp == 0
|
84
|
+
return idx
|
85
|
+
elsif comp > 0
|
86
|
+
upper = idx - 1
|
87
|
+
else
|
88
|
+
lower = idx + 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
return upper
|
92
|
+
end
|
93
|
+
|
94
|
+
class Entry
|
95
|
+
attr_reader :value
|
96
|
+
attr_reader :server
|
97
|
+
|
98
|
+
def initialize(val, srv)
|
99
|
+
@value = val
|
100
|
+
@server = srv
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|