dalli 0.9.0 → 0.9.1
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 +5 -1
- data/History.md +14 -0
- data/Performance.md +19 -18
- data/README.md +14 -1
- data/TODO.md +6 -0
- data/dalli.gemspec +5 -2
- data/lib/dalli.rb +10 -4
- data/lib/dalli/client.rb +58 -12
- data/lib/dalli/ring.rb +12 -8
- data/lib/dalli/sasl/anonymous.rb +14 -0
- data/lib/dalli/sasl/base.rb +89 -0
- data/lib/dalli/sasl/base64.rb +14 -0
- data/lib/dalli/sasl/digest_md5.rb +175 -0
- data/lib/dalli/sasl/plain.rb +18 -0
- data/lib/dalli/server.rb +124 -12
- data/lib/dalli/version.rb +1 -1
- data/test/helper.rb +8 -0
- data/test/memcached_mock.rb +49 -3
- data/test/test_active_support.rb +60 -47
- data/test/test_benchmark.rb +79 -76
- data/test/test_dalli.rb +230 -69
- data/test/test_network.rb +5 -9
- metadata +29 -9
data/Gemfile
CHANGED
data/History.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Dalli Changelog
|
2
|
+
=====================
|
3
|
+
|
4
|
+
0.9.1
|
5
|
+
----
|
6
|
+
|
7
|
+
- Add fetch and cas operations (mperham)
|
8
|
+
- Add incr and decr operations (mperham)
|
9
|
+
- Initial support for SASL authentication via the MEMCACHE_{USERNAME,PASSWORD} environment variables, needed for Heroku (mperham)
|
10
|
+
|
11
|
+
0.9.0
|
12
|
+
-----
|
13
|
+
|
14
|
+
- Initial gem release.
|
data/Performance.md
CHANGED
@@ -2,23 +2,24 @@ Performance
|
|
2
2
|
====================
|
3
3
|
|
4
4
|
Caching is all about performance, so I carefully track Dalli performance to ensure no regressions.
|
5
|
+
Times are from a Unibody MBP 2.4Ghz Core 2 Duo running Snow Leopard.
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
Testing 1.8.5 with ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
|
8
|
+
user system total real
|
9
|
+
set:plain:memcache-client 2.070000 0.590000 2.660000 ( 2.669744)
|
10
|
+
set:ruby:memcache-client 2.150000 0.570000 2.720000 ( 2.734616)
|
11
|
+
get:plain:memcache-client 2.240000 0.400000 2.640000 ( 2.675747)
|
12
|
+
get:ruby:memcache-client 2.290000 0.380000 2.670000 ( 2.682108)
|
13
|
+
multiget:ruby:memcache-client 1.030000 0.140000 1.170000 ( 1.174503)
|
14
|
+
missing:ruby:memcache-client 1.900000 0.370000 2.270000 ( 2.282264)
|
15
|
+
mixed:ruby:memcache-client 4.430000 0.950000 5.380000 ( 5.420251)
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
Testing 0.9.0 with ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
|
18
|
+
user system total real
|
19
|
+
set:plain:dalli 1.610000 0.360000 1.970000 ( 2.032947)
|
20
|
+
set:ruby:dalli 1.690000 0.360000 2.050000 ( 2.108120)
|
21
|
+
get:plain:dalli 1.710000 0.400000 2.110000 ( 2.123895)
|
22
|
+
get:ruby:dalli 1.760000 0.390000 2.150000 ( 2.170964)
|
23
|
+
multiget:ruby:dalli 0.950000 0.310000 1.260000 ( 1.269679)
|
24
|
+
missing:ruby:dalli 1.650000 0.380000 2.030000 ( 2.054383)
|
25
|
+
mixed:ruby:dalli 3.470000 0.750000 4.220000 ( 4.323265)
|
data/README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
Dalli
|
2
2
|
=========
|
3
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.
|
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. It should be considered a replacement for the memcache-client gem. The API tries to be mostly compatible with memcache-client with the goal being to make it a drop-in replacement for Rails.
|
5
5
|
|
6
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
7
|
|
8
|
+
![Persistence of Memory](http://www.virtualdali.com/assets/paintings/31PersistenceOfMemory.jpg)
|
9
|
+
|
8
10
|
Dalli's development is sponsored by [NorthScale](http://www.northscale.com/). Many thanks to them!
|
9
11
|
|
10
12
|
|
@@ -24,11 +26,14 @@ So a few notes. Dalli:
|
|
24
26
|
1. is approximately 20% faster than memcache-client (which itself was heavily optimized) in Ruby 1.9.2.
|
25
27
|
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
28
|
3. comes with hooks to replace memcache-client in Rails.
|
29
|
+
4. is approx 700 lines of Ruby. memcache-client is approx 1250 lines.
|
27
30
|
|
28
31
|
|
29
32
|
Installation and Usage
|
30
33
|
------------------------
|
31
34
|
|
35
|
+
Remember, Dalli **requires** memcached 1.4+. You can check the version with `memcached -h`.
|
36
|
+
|
32
37
|
gem install dalli
|
33
38
|
|
34
39
|
require 'dalli'
|
@@ -36,6 +41,8 @@ Installation and Usage
|
|
36
41
|
dc.set('abc', 123)
|
37
42
|
value = dc.get('abc')
|
38
43
|
|
44
|
+
The test suite requires memcached 1.4.3+ with SASL enabled (./configure --enable-sasl). Currently only supports the PLAIN mechanism.
|
45
|
+
|
39
46
|
|
40
47
|
Usage with Rails
|
41
48
|
---------------------------
|
@@ -63,6 +70,12 @@ By default, Dalli is thread-safe. Disable thread-safety at your own peril.
|
|
63
70
|
Note that Dalli does not require ActiveSupport or Rails. You can safely use it in your own Ruby projects.
|
64
71
|
|
65
72
|
|
73
|
+
Helping Out
|
74
|
+
-------------
|
75
|
+
|
76
|
+
If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update History.md with a one sentence description of your fix so you get credit as a contributor.
|
77
|
+
|
78
|
+
|
66
79
|
Author
|
67
80
|
----------
|
68
81
|
|
data/TODO.md
ADDED
data/dalli.gemspec
CHANGED
@@ -7,12 +7,14 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Dalli::VERSION
|
8
8
|
|
9
9
|
s.authors = ["Mike Perham"]
|
10
|
-
s.date = %q{2010-08-
|
10
|
+
s.date = %q{2010-08-28}
|
11
11
|
s.description = %q{High performance memcached client for Ruby}
|
12
12
|
s.email = %q{mperham@gmail.com}
|
13
13
|
s.files = Dir.glob("lib/**/*") + [
|
14
14
|
"LICENSE",
|
15
15
|
"README.md",
|
16
|
+
"History.md",
|
17
|
+
"TODO.md",
|
16
18
|
"Rakefile",
|
17
19
|
"Gemfile",
|
18
20
|
"dalli.gemspec",
|
@@ -24,7 +26,8 @@ Gem::Specification.new do |s|
|
|
24
26
|
s.summary = %q{High performance memcached client for Ruby}
|
25
27
|
s.test_files = Dir.glob("test/**/*")
|
26
28
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
27
|
-
s.add_development_dependency(%q<
|
29
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
30
|
+
s.add_development_dependency(%q<rails>, [">= 3.0.0.rc2"])
|
28
31
|
s.add_development_dependency(%q<memcache-client>, [">= 1.8.5"])
|
29
32
|
end
|
30
33
|
|
data/lib/dalli.rb
CHANGED
@@ -4,21 +4,27 @@ require 'dalli/server'
|
|
4
4
|
require 'dalli/version'
|
5
5
|
require 'dalli/options'
|
6
6
|
|
7
|
-
require 'logger'
|
8
|
-
|
9
7
|
module Dalli
|
10
|
-
#
|
8
|
+
# generic error
|
11
9
|
class DalliError < RuntimeError; end
|
10
|
+
# socket/server communication error
|
12
11
|
class NetworkError < DalliError; end
|
13
12
|
|
14
13
|
def self.logger
|
15
14
|
@logger ||= begin
|
16
15
|
(defined?(Rails) && Rails.logger) ||
|
17
16
|
(defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER) ||
|
18
|
-
|
17
|
+
default_logger
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
21
|
+
def self.default_logger
|
22
|
+
require 'logger'
|
23
|
+
l = Logger.new(STDOUT)
|
24
|
+
l.level = Logger::INFO
|
25
|
+
l
|
26
|
+
end
|
27
|
+
|
22
28
|
def self.logger=(logger)
|
23
29
|
@logger = logger
|
24
30
|
end
|
data/lib/dalli/client.rb
CHANGED
@@ -12,19 +12,19 @@ module Dalli
|
|
12
12
|
# Both weight and port are optional.
|
13
13
|
#
|
14
14
|
# Options:
|
15
|
+
# :failover - if a server is down, store the value on another server. Default: true.
|
15
16
|
# :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
|
16
17
|
# :marshal - ensure that the value you store is exactly what is returned. Otherwise you can see this:
|
17
18
|
# set('abc', 123)
|
18
19
|
# get('abc') ==> '123' (Note you set an Integer but got back a String)
|
19
20
|
# Default: true.
|
20
21
|
#
|
21
|
-
def initialize(servers, options={})
|
22
|
+
def initialize(servers=nil, options={})
|
22
23
|
@ring = Dalli::Ring.new(
|
23
|
-
Array(servers).map do |s|
|
24
|
+
Array(servers || env_servers).map do |s|
|
24
25
|
Dalli::Server.new(s)
|
25
|
-
end
|
26
|
+
end, options
|
26
27
|
)
|
27
|
-
@ring.threadsafe! unless options[:threadsafe] == false
|
28
28
|
self.extend(Dalli::Marshal) unless options[:marshal] == false
|
29
29
|
end
|
30
30
|
|
@@ -46,13 +46,31 @@ module Dalli
|
|
46
46
|
values.inject(values) { |memo, (k,v)| memo[k] = deserialize(v); memo }
|
47
47
|
end
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
|
+
def fetch(key, ttl=0)
|
51
|
+
val = get(key)
|
52
|
+
if val.nil? && block_given?
|
53
|
+
val = yield
|
54
|
+
add(key, val, ttl)
|
55
|
+
end
|
56
|
+
val
|
57
|
+
end
|
58
|
+
|
59
|
+
def cas(key, ttl=0, &block)
|
60
|
+
(value, cas) = perform(:cas, key)
|
61
|
+
value = (!value || value == 'Not found') ? nil : deserialize(value)
|
62
|
+
if value
|
63
|
+
newvalue = block.call(value)
|
64
|
+
perform(:add, key, serialize(newvalue), ttl, cas)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
50
68
|
def set(key, value, ttl=0)
|
51
69
|
perform(:set, key, serialize(value), ttl)
|
52
70
|
end
|
53
71
|
|
54
72
|
def add(key, value, ttl=0)
|
55
|
-
perform(:add, key, serialize(value), ttl)
|
73
|
+
perform(:add, key, serialize(value), ttl, 0)
|
56
74
|
end
|
57
75
|
|
58
76
|
def replace(key, value, ttl=0)
|
@@ -64,11 +82,11 @@ module Dalli
|
|
64
82
|
end
|
65
83
|
|
66
84
|
def append(key, value)
|
67
|
-
perform(:append, key, value)
|
85
|
+
perform(:append, key, value.to_s)
|
68
86
|
end
|
69
87
|
|
70
88
|
def prepend(key, value)
|
71
|
-
perform(:prepend, key, value)
|
89
|
+
perform(:prepend, key, value.to_s)
|
72
90
|
end
|
73
91
|
|
74
92
|
def flush(delay=0)
|
@@ -80,12 +98,34 @@ module Dalli
|
|
80
98
|
flush(0)
|
81
99
|
end
|
82
100
|
|
83
|
-
|
84
|
-
|
101
|
+
##
|
102
|
+
# Incr adds the given amount to the counter on the memcached server.
|
103
|
+
# Amt must be a positive value.
|
104
|
+
#
|
105
|
+
# memcached counters are unsigned and cannot hold negative values. Calling
|
106
|
+
# decr on a counter which is 0 will just return 0.
|
107
|
+
#
|
108
|
+
# If default is nil, the counter must already exist or the operation
|
109
|
+
# will fail and will return nil. Otherwise this method will return
|
110
|
+
# the new value for the counter.
|
111
|
+
def incr(key, amt, ttl=0, default=nil)
|
112
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
113
|
+
perform(:incr, key, amt, ttl, default)
|
85
114
|
end
|
86
115
|
|
87
|
-
|
88
|
-
|
116
|
+
##
|
117
|
+
# Decr subtracts the given amount from the counter on the memcached server.
|
118
|
+
# Amt must be a positive value.
|
119
|
+
#
|
120
|
+
# memcached counters are unsigned and cannot hold negative values. Calling
|
121
|
+
# decr on a counter which is 0 will just return 0.
|
122
|
+
#
|
123
|
+
# If default is nil, the counter must already exist or the operation
|
124
|
+
# will fail and will return nil. Otherwise this method will return
|
125
|
+
# the new value for the counter.
|
126
|
+
def decr(key, amt, ttl=0, default=nil)
|
127
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
128
|
+
perform(:decr, key, amt, ttl, default)
|
89
129
|
end
|
90
130
|
|
91
131
|
def stats
|
@@ -94,6 +134,7 @@ module Dalli
|
|
94
134
|
|
95
135
|
def close
|
96
136
|
@ring.servers.map { |s| s.close }
|
137
|
+
@ring = nil
|
97
138
|
end
|
98
139
|
|
99
140
|
private
|
@@ -106,6 +147,11 @@ module Dalli
|
|
106
147
|
value
|
107
148
|
end
|
108
149
|
|
150
|
+
def env_servers
|
151
|
+
ENV['MEMCACHE_SERVERS'].split(',')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Chokepoint method for instrumentation
|
109
155
|
def perform(op, *args)
|
110
156
|
key = args.first
|
111
157
|
validate_key(key)
|
data/lib/dalli/ring.rb
CHANGED
@@ -7,7 +7,7 @@ module Dalli
|
|
7
7
|
|
8
8
|
attr_accessor :servers, :continuum
|
9
9
|
|
10
|
-
def initialize(servers)
|
10
|
+
def initialize(servers, options)
|
11
11
|
@servers = servers
|
12
12
|
if servers.size > 1
|
13
13
|
total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
|
@@ -22,6 +22,9 @@ module Dalli
|
|
22
22
|
continuum.sort { |a, b| a.value <=> b.value }
|
23
23
|
@continuum = continuum
|
24
24
|
end
|
25
|
+
|
26
|
+
threadsafe! unless options[:threadsafe] == false
|
27
|
+
@failover = options[:failover] != false
|
25
28
|
end
|
26
29
|
|
27
30
|
def server_for_key(key)
|
@@ -33,18 +36,13 @@ module Dalli
|
|
33
36
|
entryidx = self.class.binary_search(@continuum, hkey)
|
34
37
|
server = @continuum[entryidx].server
|
35
38
|
return server if server.alive?
|
39
|
+
break unless @failover
|
36
40
|
hkey = hash_for("#{try}#{key}")
|
37
41
|
end
|
38
42
|
|
39
43
|
raise Dalli::NetworkError, "No servers available"
|
40
44
|
end
|
41
45
|
|
42
|
-
def threadsafe!
|
43
|
-
@servers.each do |s|
|
44
|
-
s.extend(Dalli::Threadsafe)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
46
|
def lock
|
49
47
|
@servers.each { |s| s.lock! }
|
50
48
|
begin
|
@@ -55,7 +53,13 @@ module Dalli
|
|
55
53
|
end
|
56
54
|
|
57
55
|
private
|
58
|
-
|
56
|
+
|
57
|
+
def threadsafe!
|
58
|
+
@servers.each do |s|
|
59
|
+
s.extend(Dalli::Threadsafe)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
59
63
|
def hash_for(key)
|
60
64
|
Zlib.crc32(key)
|
61
65
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SASL
|
2
|
+
##
|
3
|
+
# SASL ANONYMOUS where you only send a username that may not get
|
4
|
+
# evaluated by the server.
|
5
|
+
#
|
6
|
+
# RFC 4505:
|
7
|
+
# http://tools.ietf.org/html/rfc4505
|
8
|
+
class Anonymous < Mechanism
|
9
|
+
def start
|
10
|
+
@state = nil
|
11
|
+
['auth', preferences.username.to_s]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module SASL
|
2
|
+
|
3
|
+
MECHANISMS = {
|
4
|
+
}
|
5
|
+
|
6
|
+
class Preferences
|
7
|
+
def authzid
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def realm
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def digest_uri
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def username
|
20
|
+
ENV['MEMCACHE_USERNAME']
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_password?
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def allow_plaintext?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def password
|
32
|
+
ENV['MEMCACHE_PASSWORD']
|
33
|
+
end
|
34
|
+
|
35
|
+
def want_anonymous?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def SASL.new(mechanisms)
|
41
|
+
mechanisms.each do |mech|
|
42
|
+
if MECHANISMS.has_key?(mech)
|
43
|
+
x = MECHANISMS[mech]
|
44
|
+
return x.new(mech, Preferences.new)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
raise NotImplementedError, "No supported mechanisms in #{mechanisms.join(',')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Common functions for mechanisms
|
53
|
+
#
|
54
|
+
# Mechanisms implement handling of methods start and receive. They
|
55
|
+
# return: [message_name, content] or nil where message_name is
|
56
|
+
# either 'auth' or 'response' and content is either a string which
|
57
|
+
# may transmitted encoded as Base64 or nil.
|
58
|
+
class Mechanism
|
59
|
+
attr_reader :preferences
|
60
|
+
attr_reader :name
|
61
|
+
|
62
|
+
def initialize(name, preferences)
|
63
|
+
@name = name
|
64
|
+
@preferences = preferences
|
65
|
+
@state = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def success?
|
69
|
+
@state == :success
|
70
|
+
end
|
71
|
+
def failure?
|
72
|
+
@state == :failure
|
73
|
+
end
|
74
|
+
|
75
|
+
def start
|
76
|
+
raise NotImplementedError
|
77
|
+
end
|
78
|
+
|
79
|
+
def receive(message_name, content)
|
80
|
+
case message_name
|
81
|
+
when 'success'
|
82
|
+
@state = :success
|
83
|
+
when 'failure'
|
84
|
+
@state = :failure
|
85
|
+
end
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|