jashmenn-dalli 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ require 'dalli/client'
2
+ require 'dalli/ring'
3
+ require 'dalli/server'
4
+ require 'dalli/socket'
5
+ require 'dalli/version'
6
+ require 'dalli/options'
7
+
8
+ unless ''.respond_to?(:bytesize)
9
+ class String
10
+ alias_method :bytesize, :size
11
+ end
12
+ end
13
+
14
+ module Dalli
15
+ # generic error
16
+ class DalliError < RuntimeError; end
17
+ # socket/server communication error
18
+ class NetworkError < DalliError; end
19
+ # no server available/alive error
20
+ class RingError < DalliError; end
21
+ # application error in marshalling
22
+ class MarshalError < DalliError; end
23
+
24
+ def self.logger
25
+ @logger ||= (rails_logger || default_logger)
26
+ end
27
+
28
+ def self.rails_logger
29
+ (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
30
+ (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
31
+ end
32
+
33
+ def self.default_logger
34
+ require 'logger'
35
+ l = Logger.new(STDOUT)
36
+ l.level = Logger::INFO
37
+ l
38
+ end
39
+
40
+ def self.logger=(logger)
41
+ @logger = logger
42
+ end
43
+ end
@@ -0,0 +1,264 @@
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
+ # Options:
18
+ # - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
19
+ # - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
20
+ # - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever
21
+ # - :compression - defaults to false, if true Dalli will compress values larger than 100 bytes before
22
+ # sending them to memcached.
23
+ #
24
+ def initialize(servers=nil, options={})
25
+ @servers = env_servers || servers || 'localhost:11211'
26
+ @options = { :expires_in => 0 }.merge(options)
27
+ self.extend(Dalli::Client::MemcacheClientCompatibility) if Dalli::Client.compatibility_mode
28
+ @ring = nil
29
+ end
30
+
31
+ ##
32
+ # Turn on compatibility mode, which mixes in methods in memcache_client_compatibility.rb
33
+ # This value is set to true in memcache-client.rb.
34
+ def self.compatibility_mode
35
+ @compatibility_mode ||= false
36
+ end
37
+
38
+ def self.compatibility_mode=(compatibility_mode)
39
+ require 'dalli/compatibility'
40
+ @compatibility_mode = compatibility_mode
41
+ end
42
+
43
+ #
44
+ # The standard memcached instruction set
45
+ #
46
+
47
+ ##
48
+ # Turn on quiet aka noreply support.
49
+ # All relevant operations within this block with be effectively
50
+ # pipelined as Dalli will use 'quiet' operations where possible.
51
+ # Currently supports the set, add, replace and delete operations.
52
+ def multi
53
+ old, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true
54
+ yield
55
+ ensure
56
+ Thread.current[:dalli_multi] = old
57
+ end
58
+
59
+ def get(key, options=nil)
60
+ resp = perform(:get, key)
61
+ (!resp || resp == 'Not found') ? nil : resp
62
+ end
63
+
64
+ ##
65
+ # Fetch multiple keys efficiently.
66
+ # Returns a hash of { 'key' => 'value', 'key2' => 'value1' }
67
+ def get_multi(*keys)
68
+ return {} if keys.empty?
69
+ options = nil
70
+ options = keys.pop if keys.last.is_a?(Hash) || keys.last.nil?
71
+ ring.lock do
72
+ keys.flatten.each do |key|
73
+ perform(:getkq, key)
74
+ end
75
+
76
+ values = {}
77
+ ring.servers.each do |server|
78
+ next unless server.alive?
79
+ begin
80
+ server.request(:noop).each_pair do |key, value|
81
+ values[key_without_namespace(key)] = value
82
+ end
83
+ rescue NetworkError => e
84
+ Dalli.logger.debug { e.message }
85
+ Dalli.logger.debug { "results from this server will be missing" }
86
+ end
87
+ end
88
+ values
89
+ end
90
+ end
91
+
92
+ def fetch(key, ttl=nil, options=nil)
93
+ ttl ||= @options[:expires_in]
94
+ val = get(key, options)
95
+ if val.nil? && block_given?
96
+ val = yield
97
+ add(key, val, ttl, options)
98
+ end
99
+ val
100
+ end
101
+
102
+ ##
103
+ # compare and swap values using optimistic locking.
104
+ # Fetch the existing value for key.
105
+ # If it exists, yield the value to the block.
106
+ # Add the block's return value as the new value for the key.
107
+ # Add will fail if someone else changed the value.
108
+ #
109
+ # Returns:
110
+ # - nil if the key did not exist.
111
+ # - false if the value was changed by someone else.
112
+ # - true if the value was successfully updated.
113
+ def cas(key, ttl=nil, options=nil, &block)
114
+ ttl ||= @options[:expires_in]
115
+ (value, cas) = perform(:cas, key)
116
+ value = (!value || value == 'Not found') ? nil : value
117
+ if value
118
+ newvalue = block.call(value)
119
+ perform(:add, key, newvalue, ttl, cas, options)
120
+ end
121
+ end
122
+
123
+ def set(key, value, ttl=nil, options=nil)
124
+ raise "Invalid API usage, please require 'dalli/memcache-client' for compatibility, see Upgrade.md" if options == true
125
+ ttl ||= @options[:expires_in]
126
+ perform(:set, key, value, ttl, options)
127
+ end
128
+
129
+ ##
130
+ # Conditionally add a key/value pair, if the key does not already exist
131
+ # on the server. Returns true if the operation succeeded.
132
+ def add(key, value, ttl=nil, options=nil)
133
+ ttl ||= @options[:expires_in]
134
+ perform(:add, key, value, ttl, 0, options)
135
+ end
136
+
137
+ ##
138
+ # Conditionally add a key/value pair, only if the key already exists
139
+ # on the server. Returns true if the operation succeeded.
140
+ def replace(key, value, ttl=nil, options=nil)
141
+ ttl ||= @options[:expires_in]
142
+ perform(:replace, key, value, ttl, options)
143
+ end
144
+
145
+ def delete(key)
146
+ perform(:delete, key)
147
+ end
148
+
149
+ def append(key, value)
150
+ perform(:append, key, value.to_s)
151
+ end
152
+
153
+ def prepend(key, value)
154
+ perform(:prepend, key, value.to_s)
155
+ end
156
+
157
+ def flush(delay=0)
158
+ time = -delay
159
+ ring.servers.map { |s| s.request(:flush, time += delay) }
160
+ end
161
+
162
+ # deprecated, please use #flush.
163
+ alias_method :flush_all, :flush
164
+
165
+ ##
166
+ # Incr adds the given amount to the counter on the memcached server.
167
+ # Amt must be a positive value.
168
+ #
169
+ # memcached counters are unsigned and cannot hold negative values. Calling
170
+ # decr on a counter which is 0 will just return 0.
171
+ #
172
+ # If default is nil, the counter must already exist or the operation
173
+ # will fail and will return nil. Otherwise this method will return
174
+ # the new value for the counter.
175
+ def incr(key, amt=1, ttl=nil, default=nil)
176
+ raise ArgumentError, "Positive values only: #{amt}" if amt < 0
177
+ ttl ||= @options[:expires_in]
178
+ perform(:incr, key, amt, ttl, default)
179
+ end
180
+
181
+ ##
182
+ # Decr subtracts the given amount from the counter on the memcached server.
183
+ # Amt must be a positive value.
184
+ #
185
+ # memcached counters are unsigned and cannot hold negative values. Calling
186
+ # decr on a counter which is 0 will just return 0.
187
+ #
188
+ # If default is nil, the counter must already exist or the operation
189
+ # will fail and will return nil. Otherwise this method will return
190
+ # the new value for the counter.
191
+ def decr(key, amt=1, ttl=nil, default=nil)
192
+ raise ArgumentError, "Positive values only: #{amt}" if amt < 0
193
+ ttl ||= @options[:expires_in]
194
+ perform(:decr, key, amt, ttl, default)
195
+ end
196
+
197
+ ##
198
+ # Collect the stats for each server.
199
+ # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
200
+ def stats
201
+ values = {}
202
+ ring.servers.each do |server|
203
+ values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats) : nil
204
+ end
205
+ values
206
+ end
207
+
208
+ ##
209
+ # Close our connection to each server.
210
+ # If you perform another operation after this, the connections will be re-established.
211
+ def close
212
+ if @ring
213
+ @ring.servers.map { |s| s.close }
214
+ @ring = nil
215
+ end
216
+ end
217
+ alias_method :reset, :close
218
+
219
+ private
220
+
221
+ def ring
222
+ @ring ||= Dalli::Ring.new(
223
+ Array(@servers).map do |s|
224
+ Dalli::Server.new(s, @options)
225
+ end, @options
226
+ )
227
+ end
228
+
229
+ def env_servers
230
+ ENV['MEMCACHE_SERVERS'] ? ENV['MEMCACHE_SERVERS'].split(',') : nil
231
+ end
232
+
233
+ # Chokepoint method for instrumentation
234
+ def perform(op, key, *args)
235
+ key = key.to_s
236
+ validate_key(key)
237
+ key = key_with_namespace(key)
238
+ begin
239
+ server = ring.server_for_key(key)
240
+ server.request(op, key, *args)
241
+ rescue NetworkError => e
242
+ Dalli.logger.debug { e.message }
243
+ Dalli.logger.debug { "retrying request with new server" }
244
+ retry
245
+ end
246
+ end
247
+
248
+ def validate_key(key)
249
+ raise ArgumentError, "illegal character in key #{key}" if key.respond_to?(:ascii_only?) && !key.ascii_only?
250
+ raise ArgumentError, "illegal character in key #{key}" if key =~ /\s/
251
+ raise ArgumentError, "illegal character in key #{key}" if key =~ /[\x00-\x20\x80-\xFF]/
252
+ raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
253
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
254
+ end
255
+
256
+ def key_with_namespace(key)
257
+ @options[:namespace] ? "#{@options[:namespace]}:#{key}" : key
258
+ end
259
+
260
+ def key_without_namespace(key)
261
+ @options[:namespace] ? key.gsub(%r(\A#{@options[:namespace]}:), '') : key
262
+ end
263
+ end
264
+ 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,44 @@
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 request(op, *args)
13
+ lock.synchronize do
14
+ super
15
+ end
16
+ end
17
+
18
+ def alive?
19
+ lock.synchronize do
20
+ super
21
+ end
22
+ end
23
+
24
+ def close
25
+ lock.synchronize do
26
+ super
27
+ end
28
+ end
29
+
30
+ def lock!
31
+ lock.mon_enter
32
+ end
33
+
34
+ def unlock!
35
+ lock.mon_exit
36
+ end
37
+
38
+ private
39
+ def lock
40
+ @lock ||= Monitor.new
41
+ end
42
+
43
+ end
44
+ 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