memcache-client 1.0.3 → 1.1.0
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.
- 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:
|