jashmenn-dalli 1.0.3

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,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