mob-dalli 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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