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.
@@ -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
@@ -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