dalli 0.9.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Mike Perham
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Performance.md ADDED
@@ -0,0 +1,24 @@
1
+ Performance
2
+ ====================
3
+
4
+ Caching is all about performance, so I carefully track Dalli performance to ensure no regressions.
5
+
6
+ Testing 1.8.5 with ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
7
+ user system total real
8
+ set:plain:memcache-client 1.600000 0.390000 1.990000 ( 2.020491)
9
+ set:ruby:memcache-client 1.680000 0.390000 2.070000 ( 2.108217)
10
+ get:plain:memcache-client 1.740000 0.250000 1.990000 ( 2.018315)
11
+ get:ruby:memcache-client 1.790000 0.250000 2.040000 ( 2.065529)
12
+ multiget:ruby:memcache-client 0.800000 0.090000 0.890000 ( 0.914336)
13
+ missing:ruby:memcache-client 1.480000 0.250000 1.730000 ( 1.761555)
14
+ mixed:ruby:memcache-client 3.470000 0.640000 4.110000 ( 4.195236)
15
+
16
+ Testing 0.1.0 with ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
17
+ user system total real
18
+ set:plain:dalli 0.430000 0.180000 0.610000 ( 1.051395)
19
+ set:ruby:dalli 0.490000 0.180000 0.670000 ( 1.124848)
20
+ get:plain:dalli 0.490000 0.210000 0.700000 ( 1.141887)
21
+ get:ruby:dalli 0.540000 0.200000 0.740000 ( 1.188353)
22
+ multiget:ruby:dalli 0.510000 0.200000 0.710000 ( 0.772860)
23
+ missing:ruby:dalli 0.450000 0.210000 0.660000 ( 1.070748)
24
+ mixed:ruby:dalli 1.050000 0.390000 1.440000 ( 2.304933)
data/README.md ADDED
@@ -0,0 +1,75 @@
1
+ Dalli
2
+ =========
3
+
4
+ Dalli is a high performance pure Ruby client for accessing memcached servers. It works with memcached 1.4+ only as it uses the newer binary protocol. The API tries to be mostly compatible with memcache-client with the goal being to make it a drop-in replacement for Rails.
5
+
6
+ The name is a variant of Salvador Dali for his famous painting [The Persistence of Memory](http://en.wikipedia.org/wiki/The_Persistence_of_Memory).
7
+
8
+ Dalli's development is sponsored by [NorthScale](http://www.northscale.com/). Many thanks to them!
9
+
10
+
11
+ Design
12
+ ------------
13
+
14
+ I decided to write Dalli after maintaining memcache-client for the last two years for a few specific reasons:
15
+
16
+ 0. The code is mostly old and gross. The bulk of the code is a single 1000 line .rb file.
17
+ 1. It has a lot of options that are infrequently used which complicate the codebase.
18
+ 2. The implementation has no single point to attach monitoring hooks.
19
+ 3. Uses the old text protocol, which hurts raw performance.
20
+
21
+ So a few notes. Dalli:
22
+
23
+ 0. uses the exact same algorithm to choose a server so existing memcached clusters with TBs of data will work identically to memcache-client.
24
+ 1. is approximately 20% faster than memcache-client (which itself was heavily optimized) in Ruby 1.9.2.
25
+ 2. contains explicit "chokepoint" methods which handle all requests; these can be hooked into by monitoring tools (NewRelic, Rack::Bug, etc) to track memcached usage.
26
+ 3. comes with hooks to replace memcache-client in Rails.
27
+
28
+
29
+ Installation and Usage
30
+ ------------------------
31
+
32
+ gem install dalli
33
+
34
+ require 'dalli'
35
+ dc = Dalli::Client.new('localhost:11211')
36
+ dc.set('abc', 123)
37
+ value = dc.get('abc')
38
+
39
+
40
+ Usage with Rails
41
+ ---------------------------
42
+
43
+ In your Gemfile:
44
+
45
+ gem 'dalli'
46
+
47
+ In `config/environments/production.rb`. Note that we are also setting a reasonable default for maximum cache entry lifetime (one day), enabling compression for large values, and namespacing all entries for this rails app. Remove the namespace if you have multiple apps which share cached values.
48
+
49
+ require 'active_support/cache/dalli_store'
50
+ config.cache_store = :dalli_store, 'cache-1.example.com', 'cache-2.example.com',
51
+ :namespace => NAME_OF_RAILS_APP, :expires_in => 1.day, :compress => true, :compress_threshold => 64.kilobytes
52
+
53
+
54
+ Features and Changes
55
+ ------------------------
56
+
57
+ memcache-client allowed developers to store either raw or marshalled values with each API call. I feel this is needless complexity; Dalli allows you to control marshalling per-Client with the `:marshal => false` flag but you cannot explicitly set the raw flag for each API call. By default, marshalling is enabled.
58
+
59
+ I've removed support for key namespaces and automatic pruning of keys longer than 250 characters. ActiveSupport::Cache implements these features so there is little need for Dalli to reinvent them.
60
+
61
+ By default, Dalli is thread-safe. Disable thread-safety at your own peril.
62
+
63
+ Note that Dalli does not require ActiveSupport or Rails. You can safely use it in your own Ruby projects.
64
+
65
+
66
+ Author
67
+ ----------
68
+
69
+ Mike Perham, mperham@gmail.com, [mikeperham.com](http://mikeperham.com), [@mperham](http://twitter.com/mperham) If you like and use this project, please give me a recommendation at [WWR](http://workingwithrails.com/person/10797-mike-perham). Happy caching!
70
+
71
+
72
+ Copyright
73
+ -----------
74
+
75
+ Copyright (c) 2010 Mike Perham. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new(:test) do |test|
3
+ test.libs << 'test'
4
+ test.pattern = 'test/**/test_*.rb'
5
+ end
6
+
7
+ task :default => :test
data/dalli.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+ require 'dalli/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{dalli}
7
+ s.version = Dalli::VERSION
8
+
9
+ s.authors = ["Mike Perham"]
10
+ s.date = %q{2010-08-19}
11
+ s.description = %q{High performance memcached client for Ruby}
12
+ s.email = %q{mperham@gmail.com}
13
+ s.files = Dir.glob("lib/**/*") + [
14
+ "LICENSE",
15
+ "README.md",
16
+ "Rakefile",
17
+ "Gemfile",
18
+ "dalli.gemspec",
19
+ "Performance.md",
20
+ ]
21
+ s.homepage = %q{http://github.com/mperham/dalli}
22
+ s.rdoc_options = ["--charset=UTF-8"]
23
+ s.require_paths = ["lib"]
24
+ s.summary = %q{High performance memcached client for Ruby}
25
+ s.test_files = Dir.glob("test/**/*")
26
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
27
+ s.add_development_dependency(%q<rails>, [">= 3.0.0.rc"])
28
+ s.add_development_dependency(%q<memcache-client>, [">= 1.8.5"])
29
+ end
30
+
@@ -0,0 +1,177 @@
1
+ begin
2
+ require 'dalli'
3
+ rescue LoadError => e
4
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
5
+ raise e
6
+ end
7
+ require 'digest/md5'
8
+
9
+ module ActiveSupport
10
+ module Cache
11
+ # A cache store implementation which stores data in Memcached:
12
+ # http://www.danga.com/memcached/
13
+ #
14
+ # DalliStore implements the Strategy::LocalCache strategy which implements
15
+ # an in memory cache inside of a block.
16
+ class DalliStore < Store
17
+ module Response # :nodoc:
18
+ STORED = "STORED\r\n"
19
+ NOT_STORED = "NOT_STORED\r\n"
20
+ EXISTS = "EXISTS\r\n"
21
+ NOT_FOUND = "NOT_FOUND\r\n"
22
+ DELETED = "DELETED\r\n"
23
+ end
24
+
25
+ ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/
26
+
27
+ def self.build_mem_cache(*addresses)
28
+ addresses = addresses.flatten
29
+ options = addresses.extract_options!
30
+ addresses = ["localhost:11211"] if addresses.empty?
31
+ Dalli::Client.new(addresses, options)
32
+ end
33
+
34
+ # Creates a new DalliStore object, with the given memcached server
35
+ # addresses. Each address is either a host name, or a host-with-port string
36
+ # in the form of "host_name:port". For example:
37
+ #
38
+ # ActiveSupport::Cache::DalliStore.new("localhost", "server-downstairs.localnetwork:8229")
39
+ #
40
+ # If no addresses are specified, then DalliStore will connect to
41
+ # localhost port 11211 (the default memcached port).
42
+ #
43
+ def initialize(*addresses)
44
+ addresses = addresses.flatten
45
+ options = addresses.extract_options!
46
+ super(options)
47
+
48
+ if addresses.first.respond_to?(:get)
49
+ @data = addresses.first
50
+ else
51
+ mem_cache_options = options.dup
52
+ UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
53
+ @data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
54
+ end
55
+
56
+ extend Strategy::LocalCache
57
+ extend LocalCacheWithRaw
58
+ end
59
+
60
+ # Reads multiple keys from the cache using a single call to the
61
+ # servers for all keys. Options can be passed in the last argument.
62
+ def read_multi(*names)
63
+ options = names.extract_options!
64
+ options = merged_options(options)
65
+ keys_to_names = names.inject({}){|map, name| map[escape_key(namespaced_key(name, options))] = name; map}
66
+ raw_values = @data.get_multi(keys_to_names.keys)
67
+ values = {}
68
+ raw_values.each do |key, value|
69
+ entry = deserialize_entry(value)
70
+ values[keys_to_names[key]] = entry.value unless entry.expired?
71
+ end
72
+ values
73
+ end
74
+
75
+ # Increment a cached value. This method uses the memcached incr atomic
76
+ # operator and can only be used on values written with the :raw option.
77
+ # Calling it on a value not stored with :raw will initialize that value
78
+ # to zero.
79
+ def increment(name, amount = 1, options = nil) # :nodoc:
80
+ options = merged_options(options)
81
+ response = instrument(:increment, name, :amount => amount) do
82
+ @data.incr(escape_key(namespaced_key(name, options)), amount)
83
+ end
84
+ response == Response::NOT_FOUND ? nil : response.to_i
85
+ rescue Dalli::DalliError
86
+ nil
87
+ end
88
+
89
+ # Decrement a cached value. This method uses the memcached decr atomic
90
+ # operator and can only be used on values written with the :raw option.
91
+ # Calling it on a value not stored with :raw will initialize that value
92
+ # to zero.
93
+ def decrement(name, amount = 1, options = nil) # :nodoc:
94
+ options = merged_options(options)
95
+ response = instrument(:decrement, name, :amount => amount) do
96
+ @data.decr(escape_key(namespaced_key(name, options)), amount)
97
+ end
98
+ response == Response::NOT_FOUND ? nil : response.to_i
99
+ rescue Dalli::DalliError
100
+ nil
101
+ end
102
+
103
+ # Clear the entire cache on all memcached servers. This method should
104
+ # be used with care when using a shared cache.
105
+ def clear(options = nil)
106
+ @data.flush_all
107
+ end
108
+
109
+ # Get the statistics from the memcached servers.
110
+ def stats
111
+ @data.stats
112
+ end
113
+
114
+ protected
115
+ # Read an entry from the cache.
116
+ def read_entry(key, options) # :nodoc:
117
+ deserialize_entry(@data.get(escape_key(key)))
118
+ rescue Dalli::DalliError => e
119
+ logger.error("DalliError (#{e}): #{e.message}") if logger
120
+ nil
121
+ end
122
+
123
+ # Write an entry to the cache.
124
+ def write_entry(key, entry, options) # :nodoc:
125
+ method = options && options[:unless_exist] ? :add : :set
126
+ value = options[:raw] ? entry.value.to_s : entry
127
+ expires_in = options[:expires_in].to_i
128
+ if expires_in > 0 && !options[:raw]
129
+ # Set the memcache expire a few minutes in the future to support race condition ttls on read
130
+ expires_in += 5.minutes
131
+ end
132
+ @data.send(method, escape_key(key), value, expires_in)
133
+ rescue Dalli::DalliError => e
134
+ logger.error("DalliError (#{e}): #{e.message}") if logger
135
+ false
136
+ end
137
+
138
+ # Delete an entry from the cache.
139
+ def delete_entry(key, options) # :nodoc:
140
+ @data.delete(escape_key(key))
141
+ rescue Dalli::DalliError => e
142
+ logger.error("DalliError (#{e}): #{e.message}") if logger
143
+ false
144
+ end
145
+
146
+ private
147
+ def escape_key(key)
148
+ key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match.getbyte(0).to_s(16).upcase}"}
149
+ key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
150
+ key
151
+ end
152
+
153
+ def deserialize_entry(raw_value)
154
+ if raw_value
155
+ entry = Marshal.load(raw_value) rescue raw_value
156
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
157
+ else
158
+ nil
159
+ end
160
+ end
161
+
162
+ # Provide support for raw values in the local cache strategy.
163
+ module LocalCacheWithRaw # :nodoc:
164
+ protected
165
+ def write_entry(key, entry, options) # :nodoc:
166
+ retval = super
167
+ if options[:raw] && local_cache && retval
168
+ raw_entry = Entry.new(entry.value.to_s)
169
+ raw_entry.expires_at = entry.expires_at
170
+ local_cache.write_entry(key, raw_entry, options)
171
+ end
172
+ retval
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
data/lib/dalli.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'dalli/client'
2
+ require 'dalli/ring'
3
+ require 'dalli/server'
4
+ require 'dalli/version'
5
+ require 'dalli/options'
6
+
7
+ require 'logger'
8
+
9
+ module Dalli
10
+ # socket communication error
11
+ class DalliError < RuntimeError; end
12
+ class NetworkError < DalliError; end
13
+
14
+ def self.logger
15
+ @logger ||= begin
16
+ (defined?(Rails) && Rails.logger) ||
17
+ (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER) ||
18
+ (l = Logger.new(STDOUT); l.level = Logger::INFO; l)
19
+ end
20
+ end
21
+
22
+ def self.logger=(logger)
23
+ @logger = logger
24
+ end
25
+ end
@@ -0,0 +1,122 @@
1
+ module Dalli
2
+ class Client
3
+
4
+ ##
5
+ # Dalli::Client is the main class which developers will use to interact with
6
+ # the memcached server. Usage:
7
+ # <pre>
8
+ # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
9
+ # :threadsafe => false, :marshal => false)
10
+ # </pre>
11
+ # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
12
+ # Both weight and port are optional.
13
+ #
14
+ # Options:
15
+ # :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
16
+ # :marshal - ensure that the value you store is exactly what is returned. Otherwise you can see this:
17
+ # set('abc', 123)
18
+ # get('abc') ==> '123' (Note you set an Integer but got back a String)
19
+ # Default: true.
20
+ #
21
+ def initialize(servers, options={})
22
+ @ring = Dalli::Ring.new(
23
+ Array(servers).map do |s|
24
+ Dalli::Server.new(s)
25
+ end
26
+ )
27
+ @ring.threadsafe! unless options[:threadsafe] == false
28
+ self.extend(Dalli::Marshal) unless options[:marshal] == false
29
+ end
30
+
31
+ #
32
+ # The standard memcached instruction set
33
+ #
34
+
35
+ def get(key)
36
+ resp = perform(:get, key)
37
+ (!resp || resp == 'Not found') ? nil : deserialize(resp)
38
+ end
39
+
40
+ def get_multi(*keys)
41
+ @ring.lock do
42
+ keys.flatten.each do |key|
43
+ perform(:getkq, key)
44
+ end
45
+ values = @ring.servers.inject({}) { |hash, s| hash.merge!(s.request(:noop)); hash }
46
+ values.inject(values) { |memo, (k,v)| memo[k] = deserialize(v); memo }
47
+ end
48
+ end
49
+
50
+ def set(key, value, ttl=0)
51
+ perform(:set, key, serialize(value), ttl)
52
+ end
53
+
54
+ def add(key, value, ttl=0)
55
+ perform(:add, key, serialize(value), ttl)
56
+ end
57
+
58
+ def replace(key, value, ttl=0)
59
+ perform(:replace, key, serialize(value), ttl)
60
+ end
61
+
62
+ def delete(key)
63
+ perform(:delete, key)
64
+ end
65
+
66
+ def append(key, value)
67
+ perform(:append, key, value)
68
+ end
69
+
70
+ def prepend(key, value)
71
+ perform(:prepend, key, value)
72
+ end
73
+
74
+ def flush(delay=0)
75
+ time = -delay
76
+ @ring.servers.map { |s| s.request(:flush, time += delay) }
77
+ end
78
+
79
+ def flush_all
80
+ flush(0)
81
+ end
82
+
83
+ def incr(key, amt)
84
+ perform(:incr, key, amt)
85
+ end
86
+
87
+ def decr(key, amt)
88
+ perform(:decr, key, amt)
89
+ end
90
+
91
+ def stats
92
+ @ring.servers.inject({}) { |memo, s| memo["#{s.hostname}:#{s.port}"] = s.request(:stats); memo }
93
+ end
94
+
95
+ def close
96
+ @ring.servers.map { |s| s.close }
97
+ end
98
+
99
+ private
100
+
101
+ def serialize(value)
102
+ value.to_s
103
+ end
104
+
105
+ def deserialize(value)
106
+ value
107
+ end
108
+
109
+ def perform(op, *args)
110
+ key = args.first
111
+ validate_key(key)
112
+ server = @ring.server_for_key(key)
113
+ server.request(op, *args)
114
+ end
115
+
116
+ def validate_key(key)
117
+ raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/
118
+ raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
119
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
120
+ end
121
+ end
122
+ end