memcache-client 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +12 -0
- data/LICENSE.txt +30 -0
- data/Manifest.txt +4 -1
- data/{README → README.txt} +0 -0
- data/Rakefile +11 -48
- data/lib/memcache.rb +394 -310
- data/lib/memcache_util.rb +28 -25
- data/test/test_memcache.rb +221 -0
- metadata +23 -10
data/History.txt
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
All original code copyright 2005 Bob Cottrell, The Robot Co-op. All rights
|
2
|
+
reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions
|
6
|
+
are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
3. Neither the names of the authors nor the names of their contributors
|
14
|
+
may be used to endorse or promote products derived from this software
|
15
|
+
without specific prior written permission.
|
16
|
+
4. Redistribution in Rails or any sub-projects of Rails is not allowed
|
17
|
+
until Rails runs without warnings with the ``-W2'' flag enabled.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
20
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
23
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
24
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
25
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
26
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
27
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
28
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
29
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
data/Manifest.txt
CHANGED
data/{README → README.txt}
RENAMED
File without changes
|
data/Rakefile
CHANGED
@@ -1,55 +1,18 @@
|
|
1
|
-
|
2
|
-
require 'rake'
|
3
|
-
require 'rake/testtask'
|
4
|
-
require 'rake/rdoctask'
|
5
|
-
require 'rake/gempackagetask'
|
6
|
-
|
7
|
-
$VERBOSE = nil
|
8
|
-
|
9
|
-
spec = Gem::Specification.new do |s|
|
10
|
-
s.name = 'memcache-client'
|
11
|
-
s.version = '1.0.3'
|
12
|
-
s.summary = 'A Ruby memcached client'
|
13
|
-
s.author = 'Robert Cottrell'
|
14
|
-
s.email = 'bob@robotcoop.com'
|
15
|
-
|
16
|
-
s.has_rdoc = true
|
17
|
-
s.files = File.read('Manifest.txt').split($/)
|
18
|
-
s.require_path = 'lib'
|
19
|
-
end
|
20
|
-
|
21
|
-
desc 'Run tests'
|
22
|
-
task :default => [ :test ]
|
1
|
+
# vim: syntax=Ruby
|
23
2
|
|
24
|
-
|
25
|
-
t.libs << 'test'
|
26
|
-
t.pattern = 'test/test_*.rb'
|
27
|
-
t.verbose = true
|
28
|
-
end
|
3
|
+
require 'hoe'
|
29
4
|
|
30
|
-
|
31
|
-
task :update_manifest do
|
32
|
-
sh "find . -type f | sed -e 's%./%%' | egrep -v 'svn|swp|~' | egrep -v '^(doc|pkg)/' | sort > Manifest.txt"
|
33
|
-
end
|
5
|
+
DEV_DOC_PATH = "Libraries/memcache-client"
|
34
6
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
7
|
+
SPEC = Hoe.new 'memcache-client', '1.1.0' do |p|
|
8
|
+
p.summary = 'A Ruby memcached client'
|
9
|
+
p.description = 'memcache-client is a pure-ruby client to Danga\'s memcached.'
|
10
|
+
p.author = 'Robert Cottrell'
|
11
|
+
p.email = 'eric@robotcoop.com'
|
12
|
+
p.url = "http://dev.robotcoop.com/#{DEV_DOC_PATH}"
|
42
13
|
|
43
|
-
|
44
|
-
Rake::GemPackageTask.new spec do |pkg|
|
45
|
-
pkg.need_tar = true
|
14
|
+
p.rubyforge_name = 'rctools'
|
46
15
|
end
|
47
16
|
|
48
|
-
|
49
|
-
task :clean => [ :clobber_rdoc, :clobber_package ]
|
50
|
-
|
51
|
-
desc 'Clean up'
|
52
|
-
task :clobber => [ :clean ]
|
53
|
-
|
54
|
-
# vim: syntax=Ruby
|
17
|
+
require '../tasks'
|
55
18
|
|
data/lib/memcache.rb
CHANGED
@@ -1,361 +1,445 @@
|
|
1
|
+
$TESTING = defined? $TESTING
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
require 'thread'
|
5
|
+
require 'timeout'
|
6
|
+
require 'rubygems'
|
3
7
|
|
8
|
+
##
|
4
9
|
# A Ruby client library for memcached.
|
5
10
|
#
|
6
11
|
# This is intended to provide access to basic memcached functionality. It
|
7
12
|
# does not attempt to be complete implementation of the entire API.
|
8
|
-
|
9
|
-
# In particular, the methods of this class are not thread safe. The calling
|
10
|
-
# application is responsible for implementing any necessary locking if a cache
|
11
|
-
# object will be called from multiple threads.
|
13
|
+
|
12
14
|
class MemCache
|
13
|
-
# Patterns for matching against server error replies.
|
14
|
-
GENERAL_ERROR = /^ERROR\r\n/
|
15
|
-
CLIENT_ERROR = /^CLIENT_ERROR/
|
16
|
-
SERVER_ERROR = /^SERVER_ERROR/
|
17
|
-
|
18
|
-
# Default options for the cache object.
|
19
|
-
DEFAULT_OPTIONS = {
|
20
|
-
:namespace => nil,
|
21
|
-
:readonly => false
|
22
|
-
}
|
23
|
-
|
24
|
-
# Default memcached port.
|
25
|
-
DEFAULT_PORT = 11211
|
26
|
-
|
27
|
-
# Default memcached server weight.
|
28
|
-
DEFAULT_WEIGHT = 1
|
29
|
-
|
30
|
-
# The amount of time to wait for a response from a memcached server. If a
|
31
|
-
# response is not completed within this time, the connection to the server
|
32
|
-
# will be closed and an error will be raised.
|
33
|
-
attr_accessor :request_timeout
|
34
|
-
|
35
|
-
# Valid options are:
|
36
|
-
#
|
37
|
-
# :namespace
|
38
|
-
# If specified, all keys will have the given value prepended
|
39
|
-
# before accessing the cache. Defaults to nil.
|
40
|
-
#
|
41
|
-
# :readonly
|
42
|
-
# If this is set, any attempt to write to the cache will generate
|
43
|
-
# an exception. Defaults to false.
|
44
|
-
#
|
45
|
-
def initialize(opts = {})
|
46
|
-
opts = DEFAULT_OPTIONS.merge(opts)
|
47
|
-
@namespace = opts[:namespace]
|
48
|
-
@readonly = opts[:readonly]
|
49
|
-
@mutex = Mutex.new
|
50
|
-
@servers = []
|
51
|
-
@buckets = []
|
52
|
-
end
|
53
15
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
16
|
+
##
|
17
|
+
# Default options for the cache object.
|
18
|
+
|
19
|
+
DEFAULT_OPTIONS = {
|
20
|
+
:namespace => nil,
|
21
|
+
:readonly => false,
|
22
|
+
:multithread => false,
|
23
|
+
}
|
24
|
+
|
25
|
+
##
|
26
|
+
# Default memcached port.
|
27
|
+
|
28
|
+
DEFAULT_PORT = 11211
|
29
|
+
|
30
|
+
##
|
31
|
+
# Default memcached server weight.
|
32
|
+
|
33
|
+
DEFAULT_WEIGHT = 1
|
34
|
+
|
35
|
+
##
|
36
|
+
# The amount of time to wait for a response from a memcached server. If a
|
37
|
+
# response is not completed within this time, the connection to the server
|
38
|
+
# will be closed and an error will be raised.
|
39
|
+
|
40
|
+
attr_accessor :request_timeout
|
41
|
+
|
42
|
+
##
|
43
|
+
# The namespace for this instance
|
44
|
+
|
45
|
+
attr_reader :namespace
|
46
|
+
|
47
|
+
##
|
48
|
+
# The multithread setting for this instance
|
49
|
+
|
50
|
+
attr_reader :multithread
|
51
|
+
|
52
|
+
##
|
53
|
+
# Accepts a list of +servers+ and a list of +opts+. +servers+ may be
|
54
|
+
# omitted. See +servers=+ for acceptable server list arguments.
|
55
|
+
#
|
56
|
+
# Valid options for +opts+ are:
|
57
|
+
#
|
58
|
+
# [:namespace] Prepends this value to all keys added or retrieved.
|
59
|
+
# [:readonly] Raises an exeception on cache writes when true.
|
60
|
+
# [:multithread] Wraps cache access in a Mutex for thread safety.
|
61
|
+
|
62
|
+
def initialize(*args)
|
63
|
+
servers = []
|
64
|
+
opts = {}
|
65
|
+
|
66
|
+
case args.length
|
67
|
+
when 0 then # NOP
|
68
|
+
when 1 then
|
69
|
+
arg = args.shift
|
70
|
+
case arg
|
71
|
+
when Hash then opts = arg
|
72
|
+
when Array then servers = arg
|
73
|
+
when String then servers = [arg]
|
74
|
+
else raise ArgumentError, 'first argument must be Array, Hash or String'
|
75
|
+
end
|
76
|
+
when 2 then
|
77
|
+
servers, opts = args
|
78
|
+
else
|
79
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
|
58
80
|
end
|
59
81
|
|
60
|
-
|
61
|
-
|
62
|
-
|
82
|
+
opts = DEFAULT_OPTIONS.merge opts
|
83
|
+
@namespace = opts[:namespace]
|
84
|
+
@readonly = opts[:readonly]
|
85
|
+
@multithread = opts[:multithread]
|
86
|
+
@mutex = Mutex.new if @multithread
|
87
|
+
self.servers = servers
|
88
|
+
@buckets = []
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Return a string representation of the cache object.
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
sprintf("<MemCache: %s servers, %s buckets, ns: %p, ro: %p>",
|
96
|
+
@servers.length, @buckets.length, @namespace, @readonly)
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Returns whether there is at least one active server for the object.
|
101
|
+
|
102
|
+
def active?
|
103
|
+
not @servers.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Returns whether the cache was created read only.
|
108
|
+
|
109
|
+
def readonly?
|
110
|
+
@readonly
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Set the servers that the requests will be distributed between. Entries
|
115
|
+
# can be either strings of the form "hostname:port" or
|
116
|
+
# "hostname:port:weight" or MemCache::Server objects.
|
117
|
+
|
118
|
+
def servers=(servers)
|
119
|
+
# Create the server objects.
|
120
|
+
@servers = servers.collect do |server|
|
121
|
+
case server
|
122
|
+
when String
|
123
|
+
host, port, weight = server.split ':', 3
|
124
|
+
port ||= DEFAULT_PORT
|
125
|
+
weight ||= DEFAULT_WEIGHT
|
126
|
+
Server.new self, host, port, weight
|
127
|
+
when Server
|
128
|
+
if server.memcache.multithread != @multithread then
|
129
|
+
raise ArgumentError, "can't mix threaded and non-threaded servers"
|
130
|
+
end
|
131
|
+
server
|
132
|
+
else
|
133
|
+
raise TypeError, "Cannot convert %s to MemCache::Server" %
|
134
|
+
svr.class.name
|
135
|
+
end
|
63
136
|
end
|
64
|
-
|
65
|
-
#
|
66
|
-
|
67
|
-
|
137
|
+
|
138
|
+
# Create an array of server buckets for weight selection of servers.
|
139
|
+
@buckets = []
|
140
|
+
@servers.each do |server|
|
141
|
+
server.weight.times { @buckets.push(server) }
|
68
142
|
end
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Retrieves +key+ from memcache.
|
147
|
+
|
148
|
+
def get(key)
|
149
|
+
raise MemCacheError, 'No active servers' unless active?
|
150
|
+
cache_key = make_cache_key key
|
151
|
+
server = get_server_for_key cache_key
|
69
152
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
# Create the server objects.
|
75
|
-
@servers = servers.collect do |server|
|
76
|
-
case server
|
77
|
-
when String
|
78
|
-
host, port, weight = server.split(/:/, 3)
|
79
|
-
port ||= DEFAULT_PORT
|
80
|
-
weight ||= DEFAULT_WEIGHT
|
81
|
-
Server::new(host, port, weight)
|
82
|
-
when Server
|
83
|
-
server
|
153
|
+
raise MemCacheError, 'No connection to server' if server.socket.nil?
|
154
|
+
|
155
|
+
value = if @multithread then
|
156
|
+
threadsafe_cache_get server, cache_key
|
84
157
|
else
|
85
|
-
|
86
|
-
svr.class.name
|
158
|
+
cache_get server, cache_key
|
87
159
|
end
|
88
|
-
end
|
89
160
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
161
|
+
return nil if value.nil?
|
162
|
+
|
163
|
+
# Return the unmarshaled value.
|
164
|
+
return Marshal.load(value)
|
165
|
+
rescue ArgumentError, TypeError, SystemCallError, IOError => err
|
166
|
+
server.close
|
167
|
+
new_err = MemCacheError.new err.message
|
168
|
+
new_err.set_backtrace err.backtrace
|
169
|
+
raise new_err
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Add +key+ to the cache with value +value+ that expires in +expiry+
|
174
|
+
# seconds.
|
175
|
+
|
176
|
+
def set(key, value, expiry = 0)
|
177
|
+
raise MemCacheError, "No active servers" unless self.active?
|
178
|
+
raise MemCacheError, "Update of readonly cache" if @readonly
|
179
|
+
cache_key = make_cache_key(key)
|
180
|
+
server = get_server_for_key(cache_key)
|
181
|
+
|
182
|
+
sock = server.socket
|
183
|
+
raise MemCacheError, "No connection to server" if sock.nil?
|
184
|
+
|
185
|
+
marshaled_value = Marshal.dump value
|
186
|
+
command = "set #{cache_key} 0 #{expiry} #{marshaled_value.size}\r\n#{marshaled_value}\r\n"
|
187
|
+
|
188
|
+
begin
|
189
|
+
@mutex.synchronize do
|
190
|
+
sock.write command
|
191
|
+
sock.gets
|
192
|
+
end
|
193
|
+
rescue SystemCallError, IOError => err
|
194
|
+
server.close
|
195
|
+
raise MemCacheError, err.message
|
95
196
|
end
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# Removes +key+ from the cache in +expiry+ seconds.
|
201
|
+
|
202
|
+
def delete(key, expiry = 0)
|
203
|
+
raise MemCacheError, "No active servers" unless active?
|
204
|
+
cache_key = make_cache_key key
|
205
|
+
server = get_server_for_key cache_key
|
206
|
+
|
207
|
+
sock = server.socket
|
208
|
+
raise MemCacheError, "No connection to server" if sock.nil?
|
209
|
+
|
210
|
+
begin
|
211
|
+
@mutex.synchronize do
|
212
|
+
sock.write "delete #{cache_key} #{expiry}\r\n"
|
213
|
+
sock.gets
|
214
|
+
end
|
215
|
+
rescue SystemCallError, IOError => err
|
216
|
+
server.close
|
217
|
+
raise MemCacheError, err.message
|
218
|
+
end
|
219
|
+
end
|
96
220
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
server = get_server_for_key(cache_key)
|
221
|
+
##
|
222
|
+
# Reset the connection to all memcache servers. This should be called if
|
223
|
+
# there is a problem with a cache lookup that might have left the connection
|
224
|
+
# in a corrupted state.
|
102
225
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
226
|
+
def reset
|
227
|
+
@servers.each { |server| server.close }
|
228
|
+
end
|
107
229
|
|
108
|
-
|
109
|
-
|
110
|
-
sock.write "get #{cache_key}\r\n"
|
111
|
-
text = sock.gets # "VALUE <key> <flags> <bytes>\r\n"
|
112
|
-
return nil if text =~ /^END/ # HACK: no regex
|
113
|
-
|
114
|
-
v, cache_key, flags, bytes = text.split(/ /)
|
115
|
-
value = sock.read(bytes.to_i)
|
116
|
-
sock.read(2) # "\r\n"
|
117
|
-
sock.gets # "END\r\n"
|
118
|
-
rescue SystemCallError, IOError => err
|
119
|
-
server.close
|
120
|
-
raise MemCacheError, err.message
|
121
|
-
end
|
230
|
+
##
|
231
|
+
# Shortcut to get a value from the cache.
|
122
232
|
|
123
|
-
|
124
|
-
begin
|
125
|
-
return Marshal.load(value)
|
126
|
-
rescue ArgumentError, TypeError => err
|
127
|
-
server.close
|
128
|
-
raise MemCacheError, err.message
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
233
|
+
alias [] get
|
132
234
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
raise MemCacheError, "No active servers" unless self.active?
|
137
|
-
raise MemCacheError, "Update of readonly cache" if @readonly
|
138
|
-
cache_key = make_cache_key(key)
|
139
|
-
server = get_server_for_key(cache_key)
|
140
|
-
|
141
|
-
sock = server.socket
|
142
|
-
if sock.nil?
|
143
|
-
raise MemCacheError, "No connection to server"
|
144
|
-
end
|
235
|
+
##
|
236
|
+
# Shortcut to save a value in the cache. This method does not set an
|
237
|
+
# expiration on the entry. Use set to specify an explicit expiry.
|
145
238
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
sock.write command
|
150
|
-
sock.gets
|
151
|
-
rescue SystemCallError, IOError => err
|
152
|
-
server.close
|
153
|
-
raise MemCacheError, err.message
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
239
|
+
def []=(key, value)
|
240
|
+
set key, value
|
241
|
+
end
|
157
242
|
|
158
|
-
|
159
|
-
def delete(key, expiry = 0)
|
160
|
-
@mutex.synchronize do
|
161
|
-
raise MemCacheError, "No active servers" unless self.active?
|
162
|
-
cache_key = make_cache_key(key)
|
163
|
-
server = get_server_for_key(cache_key)
|
243
|
+
protected unless $TESTING
|
164
244
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
end
|
245
|
+
##
|
246
|
+
# Create a key for the cache, incorporating the namespace qualifier if
|
247
|
+
# requested.
|
169
248
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
raise MemCacheError, err.message
|
176
|
-
end
|
177
|
-
end
|
249
|
+
def make_cache_key(key)
|
250
|
+
if namespace.nil? then
|
251
|
+
key
|
252
|
+
else
|
253
|
+
"#{@namespace}:#{key}"
|
178
254
|
end
|
255
|
+
end
|
179
256
|
|
180
|
-
|
181
|
-
|
182
|
-
# connection in a corrupted state.
|
183
|
-
def reset
|
184
|
-
@mutex.synchronize do
|
185
|
-
@servers.each { |server| server.close }
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
# Shortcut to get a value from the cache.
|
190
|
-
def [](key)
|
191
|
-
self.get(key)
|
192
|
-
end
|
257
|
+
##
|
258
|
+
# Pick a server to handle the request based on a hash of the key.
|
193
259
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
self.set(key, value)
|
198
|
-
end
|
260
|
+
def get_server_for_key(key)
|
261
|
+
raise MemCacheError, "No servers available" if @servers.empty?
|
262
|
+
return @servers.first if @servers.length == 1
|
199
263
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
264
|
+
# Hash the value of the key to select the bucket.
|
265
|
+
hkey = key.hash
|
266
|
+
|
267
|
+
# Fetch a server for the given key, retrying if that server is offline.
|
268
|
+
20.times do |try|
|
269
|
+
server = @buckets[(hkey + try) % @buckets.nitems]
|
270
|
+
return server if server.alive?
|
205
271
|
end
|
206
272
|
|
207
|
-
|
208
|
-
|
209
|
-
# Easy enough if there is only one server.
|
210
|
-
return @servers.first if @servers.length == 1
|
211
|
-
|
212
|
-
# Hash the value of the key to select the bucket.
|
213
|
-
hkey = key.hash
|
214
|
-
|
215
|
-
# Fetch a server for the given key, retrying if that server is
|
216
|
-
# offline.
|
217
|
-
server = nil
|
218
|
-
20.times do |tries|
|
219
|
-
server = @buckets[(hkey + tries) % @buckets.nitems]
|
220
|
-
break if server.alive?
|
221
|
-
end
|
273
|
+
raise MemCacheError, "No servers available"
|
274
|
+
end
|
222
275
|
|
223
|
-
|
224
|
-
|
225
|
-
|
276
|
+
##
|
277
|
+
# Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
|
278
|
+
# miss.
|
226
279
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
# This class represents a memcached server instance.
|
233
|
-
class Server
|
234
|
-
# The amount of time to wait to establish a connection with a
|
235
|
-
# memcached server. If a connection cannot be established within
|
236
|
-
# this time limit, the server will be marked as down.
|
237
|
-
CONNECT_TIMEOUT = 0.25
|
238
|
-
|
239
|
-
# The amount of time to wait before attempting to re-establish a
|
240
|
-
# connection with a server that is marked dead.
|
241
|
-
RETRY_DELAY = 30.0
|
242
|
-
|
243
|
-
# The host the memcached server is running on.
|
244
|
-
attr_reader :host
|
245
|
-
|
246
|
-
# The port the memcached server is listening on.
|
247
|
-
attr_reader :port
|
248
|
-
|
249
|
-
# The weight given to the server.
|
250
|
-
attr_reader :weight
|
251
|
-
|
252
|
-
# The time of next retry if the connection is dead.
|
253
|
-
attr_reader :retry
|
254
|
-
|
255
|
-
# A text status string describing the state of the server.
|
256
|
-
attr_reader :status
|
257
|
-
|
258
|
-
# Create a new MemCache::Server object for the memcached instance
|
259
|
-
# listening on the given host and port, weighted by the given weight.
|
260
|
-
def initialize(host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
|
261
|
-
if host.nil? || host.empty?
|
262
|
-
raise ArgumentError, "No host specified"
|
263
|
-
elsif port.nil? || port.to_i.zero?
|
264
|
-
raise ArgumentError, "No port specified"
|
265
|
-
end
|
280
|
+
def cache_get(server, cache_key)
|
281
|
+
socket = server.socket
|
282
|
+
socket.write "get #{cache_key}\r\n"
|
283
|
+
text = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
|
284
|
+
return nil if text == "END\r\n"
|
266
285
|
|
267
|
-
|
268
|
-
|
269
|
-
|
286
|
+
text =~ /(\d+)\r/
|
287
|
+
value = socket.read $1.to_i
|
288
|
+
socket.read 2 # "\r\n"
|
289
|
+
socket.gets # "END\r\n"
|
290
|
+
return value
|
291
|
+
end
|
270
292
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
293
|
+
def threadsafe_cache_get(socket, cache_key) # :nodoc:
|
294
|
+
@mutex.lock
|
295
|
+
cache_get socket, cache_key
|
296
|
+
ensure
|
297
|
+
@mutex.unlock
|
298
|
+
end
|
275
299
|
|
276
|
-
|
277
|
-
|
278
|
-
sprintf("<MemCache::Server: %s:%d [%d] (%s)>",
|
279
|
-
@host, @port, @weight, @status)
|
280
|
-
end
|
300
|
+
##
|
301
|
+
# This class represents a memcached server instance.
|
281
302
|
|
282
|
-
|
283
|
-
# socket to attempt to connect if it isn't already connected and or if
|
284
|
-
# the server was previously marked as down and the retry time has
|
285
|
-
# been exceeded.
|
286
|
-
def alive?
|
287
|
-
!self.socket.nil?
|
288
|
-
end
|
303
|
+
class Server
|
289
304
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
unless @sock || (!@sock.nil? && @sock.closed?)
|
295
|
-
# If the host was dead, don't retry for a while.
|
296
|
-
if @retry && (@retry > Time::now)
|
297
|
-
@sock = nil
|
298
|
-
else
|
299
|
-
begin
|
300
|
-
@sock = timeout(CONNECT_TIMEOUT) {
|
301
|
-
TCPSocket::new(@host, @port)
|
302
|
-
}
|
303
|
-
@retry = nil
|
304
|
-
@status = "CONNECTED"
|
305
|
-
rescue SystemCallError, IOError, Timeout::Error => err
|
306
|
-
self.mark_dead(err.message)
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|
310
|
-
@sock
|
311
|
-
end
|
305
|
+
##
|
306
|
+
# The amount of time to wait to establish a connection with a memcached
|
307
|
+
# server. If a connection cannot be established within this time limit,
|
308
|
+
# the server will be marked as down.
|
312
309
|
|
313
|
-
|
314
|
-
# object. The server is not considered dead.
|
315
|
-
def close
|
316
|
-
@sock.close if @sock &&!@sock.closed?
|
317
|
-
@sock = nil
|
318
|
-
@retry = nil
|
319
|
-
@status = "NOT CONNECTED"
|
320
|
-
end
|
310
|
+
CONNECT_TIMEOUT = 0.25
|
321
311
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
#
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
312
|
+
##
|
313
|
+
# The amount of time to wait before attempting to re-establish a
|
314
|
+
# connection with a server that is marked dead.
|
315
|
+
|
316
|
+
RETRY_DELAY = 30.0
|
317
|
+
|
318
|
+
##
|
319
|
+
# The host the memcached server is running on.
|
320
|
+
|
321
|
+
attr_reader :host
|
322
|
+
|
323
|
+
##
|
324
|
+
# The port the memcached server is listening on.
|
325
|
+
|
326
|
+
attr_reader :port
|
327
|
+
|
328
|
+
##
|
329
|
+
# The weight given to the server.
|
330
|
+
|
331
|
+
attr_reader :weight
|
332
|
+
|
333
|
+
##
|
334
|
+
# The time of next retry if the connection is dead.
|
335
|
+
|
336
|
+
attr_reader :retry
|
337
|
+
|
338
|
+
##
|
339
|
+
# A text status string describing the state of the server.
|
340
|
+
|
341
|
+
attr_reader :status
|
342
|
+
|
343
|
+
##
|
344
|
+
# Create a new MemCache::Server object for the memcached instance
|
345
|
+
# listening on the given host and port, weighted by the given weight.
|
346
|
+
|
347
|
+
def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
|
348
|
+
raise ArgumentError, "No host specified" if host.nil? or host.empty?
|
349
|
+
raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
|
350
|
+
|
351
|
+
@memcache = memcache
|
352
|
+
@host = host
|
353
|
+
@port = port.to_i
|
354
|
+
@weight = weight.to_i
|
355
|
+
|
356
|
+
@multithread = @memcache.multithread
|
357
|
+
|
358
|
+
@sock = nil
|
359
|
+
@retry = nil
|
360
|
+
@status = 'NOT CONNECTED'
|
339
361
|
end
|
340
362
|
|
341
|
-
|
342
|
-
#
|
343
|
-
|
344
|
-
|
363
|
+
##
|
364
|
+
# Return a string representation of the server object.
|
365
|
+
|
366
|
+
def inspect
|
367
|
+
sprintf("<MemCache::Server: %s:%d [%d] (%s)>",
|
368
|
+
@host, @port, @weight, @status)
|
345
369
|
end
|
346
370
|
|
347
|
-
|
348
|
-
#
|
349
|
-
|
371
|
+
##
|
372
|
+
# Check whether the server connection is alive. This will cause the
|
373
|
+
# socket to attempt to connect if it isn't already connected and or if
|
374
|
+
# the server was previously marked as down and the retry time has
|
375
|
+
# been exceeded.
|
376
|
+
|
377
|
+
def alive?
|
378
|
+
!self.socket.nil?
|
350
379
|
end
|
351
380
|
|
352
|
-
|
353
|
-
#
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
381
|
+
##
|
382
|
+
# Try to connect to the memcached server targeted by this object.
|
383
|
+
# Returns the connected socket object on success or nil on failure.
|
384
|
+
|
385
|
+
def socket
|
386
|
+
@mutex.lock if @multithread
|
387
|
+
return @sock if @sock and not @sock.closed?
|
388
|
+
|
389
|
+
@sock = nil
|
390
|
+
|
391
|
+
# If the host was dead, don't retry for a while.
|
392
|
+
return if @retry and @retry > Time.now
|
393
|
+
|
394
|
+
# Attempt to connect if not already connected.
|
395
|
+
begin
|
396
|
+
@sock = timeout CONNECT_TIMEOUT do
|
397
|
+
TCPSocket.new @host, @port
|
359
398
|
end
|
399
|
+
@retry = nil
|
400
|
+
@status = 'CONNECTED'
|
401
|
+
rescue SystemCallError, IOError, Timeout::Error => err
|
402
|
+
mark_dead err.message
|
403
|
+
end
|
404
|
+
|
405
|
+
return @sock
|
406
|
+
ensure
|
407
|
+
@mutex.unlock if @multithread
|
360
408
|
end
|
409
|
+
|
410
|
+
##
|
411
|
+
# Close the connection to the memcached server targeted by this
|
412
|
+
# object. The server is not considered dead.
|
413
|
+
|
414
|
+
def close
|
415
|
+
@mutex.lock if @multithread
|
416
|
+
@sock.close if @sock && !@sock.closed?
|
417
|
+
@sock = nil
|
418
|
+
@retry = nil
|
419
|
+
@status = "NOT CONNECTED"
|
420
|
+
ensure
|
421
|
+
@mutex.unlock if @multithread
|
422
|
+
end
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
##
|
427
|
+
# Mark the server as dead and close its socket.
|
428
|
+
|
429
|
+
def mark_dead(reason = "Unknown error")
|
430
|
+
@sock.close if @sock && !@sock.closed?
|
431
|
+
@sock = nil
|
432
|
+
@retry = Time.now + RETRY_DELAY
|
433
|
+
|
434
|
+
@status = sprintf "DEAD: %s, will retry at %s", reason, @retry
|
435
|
+
end
|
436
|
+
|
437
|
+
end
|
438
|
+
|
439
|
+
##
|
440
|
+
# Base MemCache exception class.
|
441
|
+
|
442
|
+
class MemCacheError < RuntimeError; end
|
443
|
+
|
361
444
|
end
|
445
|
+
|
data/lib/memcache_util.rb
CHANGED
@@ -8,50 +8,53 @@ module Cache
|
|
8
8
|
# Returns the object at +key+ from the cache if successful, or nil if
|
9
9
|
# either the object is not in the cache or if there was an error
|
10
10
|
# attermpting to access the cache.
|
11
|
+
#
|
12
|
+
# If there is a cache miss and a block is given the result of the block
|
13
|
+
# will be stored in the cache with optional +expiry+.
|
11
14
|
|
12
|
-
def self.get(key)
|
13
|
-
start_time = Time.now
|
15
|
+
def self.get(key, expiry = 0)
|
16
|
+
start_time = Time.now
|
14
17
|
result = CACHE.get key
|
15
|
-
end_time = Time.now
|
16
|
-
ActiveRecord::Base.logger.debug(
|
17
|
-
|
18
|
-
end_time - start_time, key))
|
18
|
+
end_time = Time.now
|
19
|
+
ActiveRecord::Base.logger.debug('MemCache Get (%0.6f) %s' %
|
20
|
+
[end_time - start_time, key])
|
19
21
|
return result
|
20
22
|
rescue MemCache::MemCacheError => err
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
24
|
+
if block_given? then
|
25
|
+
value = yield
|
26
|
+
put key, value, expiry
|
27
|
+
return value
|
28
|
+
else
|
29
|
+
return nil
|
30
|
+
end
|
24
31
|
end
|
25
32
|
|
26
33
|
##
|
27
34
|
# Places +value+ in the cache at +key+, with an optional +expiry+ time in
|
28
|
-
# seconds.
|
35
|
+
# seconds.
|
29
36
|
|
30
37
|
def self.put(key, value, expiry = 0)
|
31
|
-
start_time = Time.now
|
38
|
+
start_time = Time.now
|
32
39
|
CACHE.set key, value, expiry
|
33
|
-
end_time = Time.now
|
34
|
-
ActiveRecord::Base.logger.debug(
|
35
|
-
|
36
|
-
end_time - start_time, key))
|
40
|
+
end_time = Time.now
|
41
|
+
ActiveRecord::Base.logger.debug('MemCache Set (%0.6f) %s' %
|
42
|
+
[end_time - start_time, key])
|
37
43
|
rescue MemCache::MemCacheError => err
|
38
|
-
|
39
|
-
ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
|
44
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
40
45
|
end
|
41
46
|
|
42
47
|
##
|
43
48
|
# Deletes +key+ from the cache in +delay+ seconds. (?)
|
44
49
|
|
45
50
|
def self.delete(key, delay = nil)
|
46
|
-
start_time = Time.now
|
51
|
+
start_time = Time.now
|
47
52
|
CACHE.delete key, delay
|
48
|
-
end_time = Time.now
|
49
|
-
ActiveRecord::Base.logger.debug(
|
50
|
-
|
51
|
-
end_time - start_time, key))
|
53
|
+
end_time = Time.now
|
54
|
+
ActiveRecord::Base.logger.debug('MemCache Delete (%0.6f) %s' %
|
55
|
+
[end_time - start_time, key])
|
52
56
|
rescue MemCache::MemCacheError => err
|
53
|
-
|
54
|
-
ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
|
57
|
+
ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
|
55
58
|
end
|
56
59
|
|
57
60
|
##
|
@@ -59,7 +62,7 @@ module Cache
|
|
59
62
|
|
60
63
|
def self.reset
|
61
64
|
CACHE.reset
|
62
|
-
ActiveRecord::Base.logger.debug
|
65
|
+
ActiveRecord::Base.logger.debug 'MemCache Connections Reset'
|
63
66
|
end
|
64
67
|
|
65
68
|
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
$TESTING = true
|
5
|
+
|
6
|
+
require 'memcache'
|
7
|
+
|
8
|
+
class MemCache
|
9
|
+
|
10
|
+
attr_reader :servers
|
11
|
+
attr_writer :namespace
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class FakeSocket
|
16
|
+
|
17
|
+
attr_reader :written, :data
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@written = StringIO.new
|
21
|
+
@data = StringIO.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def write(data)
|
25
|
+
@written.write data
|
26
|
+
end
|
27
|
+
|
28
|
+
def gets
|
29
|
+
@data.gets
|
30
|
+
end
|
31
|
+
|
32
|
+
def read(arg)
|
33
|
+
@data.read arg
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
class FakeServer
|
39
|
+
|
40
|
+
attr_reader :socket
|
41
|
+
|
42
|
+
def initialize(socket = nil)
|
43
|
+
@socket = socket || FakeSocket.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def close
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class TestMemCache < Test::Unit::TestCase
|
52
|
+
|
53
|
+
def setup
|
54
|
+
@cache = MemCache.new 'localhost:1', :namespace => 'my_namespace'
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_cache_get
|
58
|
+
server = util_setup_server
|
59
|
+
|
60
|
+
assert_equal "\004\b\"\0170123456789",
|
61
|
+
@cache.cache_get(server, 'my_namespace:key')
|
62
|
+
|
63
|
+
assert_equal "get my_namespace:key\r\n",
|
64
|
+
server.socket.written.string
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_cache_get_miss
|
68
|
+
socket = FakeSocket.new
|
69
|
+
socket.data.write "END\r\n"
|
70
|
+
socket.data.rewind
|
71
|
+
server = FakeServer.new socket
|
72
|
+
|
73
|
+
assert_equal nil, @cache.cache_get(server, 'my_namespace:key')
|
74
|
+
|
75
|
+
assert_equal "get my_namespace:key\r\n",
|
76
|
+
socket.written.string
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_initialize
|
80
|
+
cache = MemCache.new :namespace => 'my_namespace', :readonly => true
|
81
|
+
|
82
|
+
assert_equal 'my_namespace', cache.namespace
|
83
|
+
assert_equal true, cache.readonly?
|
84
|
+
assert_equal true, cache.servers.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_initialize_compatible
|
88
|
+
cache = MemCache.new ['localhost:11211', 'localhost:11212'],
|
89
|
+
:namespace => 'my_namespace', :readonly => true
|
90
|
+
|
91
|
+
assert_equal 'my_namespace', cache.namespace
|
92
|
+
assert_equal true, cache.readonly?
|
93
|
+
assert_equal false, cache.servers.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_initialize_compatible_no_hash
|
97
|
+
cache = MemCache.new ['localhost:11211', 'localhost:11212']
|
98
|
+
|
99
|
+
assert_equal nil, cache.namespace
|
100
|
+
assert_equal false, cache.readonly?
|
101
|
+
assert_equal false, cache.servers.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_initialize_compatible_one_server
|
105
|
+
cache = MemCache.new 'localhost:11211'
|
106
|
+
|
107
|
+
assert_equal nil, cache.namespace
|
108
|
+
assert_equal false, cache.readonly?
|
109
|
+
assert_equal false, cache.servers.empty?
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_initialize_compatible_bad_arg
|
113
|
+
e = assert_raise ArgumentError do
|
114
|
+
cache = MemCache.new Object.new
|
115
|
+
end
|
116
|
+
|
117
|
+
assert_equal 'first argument must be Array, Hash or String', e.message
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_initialize_too_many_args
|
121
|
+
assert_raises ArgumentError do
|
122
|
+
MemCache.new 1, 2, 3
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_get
|
127
|
+
util_setup_server
|
128
|
+
|
129
|
+
value = @cache.get 'key'
|
130
|
+
|
131
|
+
assert_equal "get my_namespace:key\r\n",
|
132
|
+
@cache.servers.first.socket.written.string
|
133
|
+
|
134
|
+
assert_equal '0123456789', value
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_get_cache_get_IOError
|
138
|
+
socket = Object.new
|
139
|
+
def socket.write(arg) raise IOError, 'some io error'; end
|
140
|
+
server = FakeServer.new socket
|
141
|
+
|
142
|
+
@cache.servers = []
|
143
|
+
@cache.servers << server
|
144
|
+
|
145
|
+
e = assert_raise MemCache::MemCacheError do
|
146
|
+
@cache.get 'my_namespace:key'
|
147
|
+
end
|
148
|
+
|
149
|
+
assert_equal 'some io error', e.message
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_get_cache_get_SystemCallError
|
153
|
+
socket = Object.new
|
154
|
+
def socket.write(arg) raise SystemCallError, 'some syscall error'; end
|
155
|
+
server = FakeServer.new socket
|
156
|
+
|
157
|
+
@cache.servers = []
|
158
|
+
@cache.servers << server
|
159
|
+
|
160
|
+
e = assert_raise MemCache::MemCacheError do
|
161
|
+
@cache.get 'my_namespace:key'
|
162
|
+
end
|
163
|
+
|
164
|
+
assert_equal 'unknown error - some syscall error', e.message
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_get_no_connection
|
168
|
+
@cache.servers = 'localhost:1'
|
169
|
+
e = assert_raise MemCache::MemCacheError do
|
170
|
+
@cache.get 'key'
|
171
|
+
end
|
172
|
+
|
173
|
+
assert_equal 'No connection to server', e.message
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_get_no_servers
|
177
|
+
@cache.servers = []
|
178
|
+
e = assert_raise MemCache::MemCacheError do
|
179
|
+
@cache.get 'key'
|
180
|
+
end
|
181
|
+
|
182
|
+
assert_equal 'No active servers', e.message
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_get_server_for_key
|
186
|
+
server = @cache.get_server_for_key 'key'
|
187
|
+
assert_equal 'localhost', server.host
|
188
|
+
assert_equal 1, server.port
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_get_server_for_key_no_servers
|
192
|
+
@cache.servers = []
|
193
|
+
|
194
|
+
e = assert_raise MemCache::MemCacheError do
|
195
|
+
@cache.get_server_for_key 'key'
|
196
|
+
end
|
197
|
+
|
198
|
+
assert_equal 'No servers available', e.message
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_make_cache_key
|
202
|
+
assert_equal 'my_namespace:key', @cache.make_cache_key('key')
|
203
|
+
@cache.namespace = nil
|
204
|
+
assert_equal 'key', @cache.make_cache_key('key')
|
205
|
+
end
|
206
|
+
|
207
|
+
def util_setup_server
|
208
|
+
server = FakeServer.new
|
209
|
+
server.socket.data.write "VALUE my_namepsace:key 0 14\r\n"
|
210
|
+
server.socket.data.write "\004\b\"\0170123456789\r\n"
|
211
|
+
server.socket.data.write "END\r\n"
|
212
|
+
server.socket.data.rewind
|
213
|
+
|
214
|
+
@cache.servers = []
|
215
|
+
@cache.servers << server
|
216
|
+
|
217
|
+
return server
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
metadata
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.8.
|
2
|
+
rubygems_version: 0.8.99
|
3
3
|
specification_version: 1
|
4
4
|
name: memcache-client
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.0
|
7
|
-
date: 2006-
|
6
|
+
version: 1.1.0
|
7
|
+
date: 2006-09-29 00:00:00 -07:00
|
8
8
|
summary: A Ruby memcached client
|
9
9
|
require_paths:
|
10
10
|
- lib
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
- test
|
12
|
+
email: eric@robotcoop.com
|
13
|
+
homepage: http://dev.robotcoop.com/Libraries/memcache-client
|
14
|
+
rubyforge_project: rctools
|
15
|
+
description: memcache-client is a pure-ruby client to Danga's memcached.
|
15
16
|
autorequire:
|
16
17
|
default_executable:
|
17
18
|
bindir: bin
|
@@ -25,14 +26,18 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
25
26
|
platform: ruby
|
26
27
|
signing_key:
|
27
28
|
cert_chain:
|
29
|
+
post_install_message:
|
28
30
|
authors:
|
29
31
|
- Robert Cottrell
|
30
32
|
files:
|
33
|
+
- History.txt
|
34
|
+
- LICENSE.txt
|
31
35
|
- Manifest.txt
|
32
|
-
- README
|
36
|
+
- README.txt
|
33
37
|
- Rakefile
|
34
38
|
- lib/memcache.rb
|
35
39
|
- lib/memcache_util.rb
|
40
|
+
- test/test_memcache.rb
|
36
41
|
test_files: []
|
37
42
|
|
38
43
|
rdoc_options: []
|
@@ -45,5 +50,13 @@ extensions: []
|
|
45
50
|
|
46
51
|
requirements: []
|
47
52
|
|
48
|
-
dependencies:
|
49
|
-
|
53
|
+
dependencies:
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: hoe
|
56
|
+
version_requirement:
|
57
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.0.0
|
62
|
+
version:
|