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 +3 -0
- data/LICENSE +20 -0
- data/Performance.md +24 -0
- data/README.md +75 -0
- data/Rakefile +7 -0
- data/dalli.gemspec +30 -0
- data/lib/active_support/cache/dalli_store.rb +177 -0
- data/lib/dalli.rb +25 -0
- data/lib/dalli/client.rb +122 -0
- data/lib/dalli/options.rb +70 -0
- data/lib/dalli/ring.rb +99 -0
- data/lib/dalli/server.rb +303 -0
- data/lib/dalli/version.rb +3 -0
- data/test/helper.rb +12 -0
- data/test/memcached_mock.rb +45 -0
- data/test/test_active_support.rb +75 -0
- data/test/test_benchmark.rb +118 -0
- data/test/test_dalli.rb +97 -0
- data/test/test_network.rb +59 -0
- metadata +130 -0
data/Gemfile
ADDED
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
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
|
data/lib/dalli/client.rb
ADDED
@@ -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
|