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.
@@ -0,0 +1,12 @@
1
+ = 1.1.0
2
+
3
+ * Added some tests
4
+ * Sped up non-multithreaded and multithreaded operation
5
+ * More Ruby-memcache compatibility
6
+ * More RDoc
7
+ * Switched to Hoe
8
+
9
+ = 1.0.0
10
+
11
+ Birthday!
12
+
@@ -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
+
@@ -1,5 +1,8 @@
1
+ History.txt
2
+ LICENSE.txt
1
3
  Manifest.txt
2
- README
4
+ README.txt
3
5
  Rakefile
4
6
  lib/memcache.rb
5
7
  lib/memcache_util.rb
8
+ test/test_memcache.rb
File without changes
data/Rakefile CHANGED
@@ -1,55 +1,18 @@
1
- require 'rubygems'
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
- Rake::TestTask.new('test') do |t|
25
- t.libs << 'test'
26
- t.pattern = 'test/test_*.rb'
27
- t.verbose = true
28
- end
3
+ require 'hoe'
29
4
 
30
- desc 'Update Manifest.txt'
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
- desc 'Generate RDoc'
36
- Rake::RDocTask.new :rdoc do |rd|
37
- rd.rdoc_dir = 'doc'
38
- rd.rdoc_files.add 'lib', 'README', 'LICENSE'
39
- rd.main = 'README'
40
- rd.options << '-d' if `which dot` =~ /\/dot/
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
- desc 'Build Gem'
44
- Rake::GemPackageTask.new spec do |pkg|
45
- pkg.need_tar = true
14
+ p.rubyforge_name = 'rctools'
46
15
  end
47
16
 
48
- desc 'Clean up'
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
 
@@ -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
- # Return a string representation of the cache object.
55
- def inspect
56
- sprintf("<MemCache: %s servers, %s buckets, ns: %p, ro: %p>",
57
- @servers.nitems, @buckets.nitems, @namespace, @readonly)
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
- # Returns whether there is at least one active server for the object.
61
- def active?
62
- not @servers.empty?
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
- # Returns whether the cache was created read only.
66
- def readonly?
67
- @readonly
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
- # Set the servers that the requests will be distributed between. Entries
71
- # can be either strings of the form "hostname:port" or
72
- # "hostname:port:weight" or MemCache::Server objects.
73
- def servers=(servers)
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
- raise TypeError, "Cannot convert %s to MemCache::Server" %
86
- svr.class.name
158
+ cache_get server, cache_key
87
159
  end
88
- end
89
160
 
90
- # Create an array of server buckets for weight selection of servers.
91
- @buckets = []
92
- @servers.each do |server|
93
- server.weight.times { @buckets.push(server) }
94
- end
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
- def get(key)
98
- @mutex.synchronize do
99
- raise MemCacheError, "No active servers" unless self.active?
100
- cache_key = make_cache_key(key)
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
- sock = server.socket
104
- if sock.nil?
105
- raise MemCacheError, "No connection to server"
106
- end
226
+ def reset
227
+ @servers.each { |server| server.close }
228
+ end
107
229
 
108
- value = nil
109
- begin
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
- # Return the unmarshaled value.
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
- # Add an entry to the cache.
134
- def set(key, value, expiry = 0)
135
- @mutex.synchronize do
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
- marshaled_value = Marshal.dump(value)
147
- command = "set #{cache_key} 0 #{expiry} #{marshaled_value.size}\r\n" + marshaled_value + "\r\n"
148
- begin
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
- # Remove an entry from the cache.
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
- sock = server.socket
166
- if sock.nil?
167
- raise MemCacheError, "No connection to server"
168
- end
245
+ ##
246
+ # Create a key for the cache, incorporating the namespace qualifier if
247
+ # requested.
169
248
 
170
- begin
171
- sock.write "delete #{cache_key} #{expiry}\r\n"
172
- sock.gets
173
- rescue SystemCallError, IOError => err
174
- server.close
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
- # Reset the connection to all memcache servers. This should be called if
181
- # there is a problem with a cache lookup that might have left the
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
- # Shortcut to save a value in the cache. This method does not set an
195
- # expiration on the entry. Use set to specify an explicit expiry.
196
- def []=(key, value)
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
- # Create a key for the cache, incorporating the namespace qualifier if
201
- # requested.
202
- protected
203
- def make_cache_key(key)
204
- @namespace.nil? ? key.to_s : "#{@namespace}:#{key}"
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
- # Pick a server to handle the request based on a hash of the key.
208
- def get_server_for_key(key)
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
- raise MemCacheError, "No servers available" unless server
224
- server
225
- end
276
+ ##
277
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
278
+ # miss.
226
279
 
227
-
228
- ###########################################################################
229
- # S E R V E R C L A S S
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
- @host = host
268
- @port = port.to_i
269
- @weight = weight.to_i
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
- @sock = nil
272
- @retry = nil
273
- @status = "NOT CONNECTED"
274
- end
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
- # Return a string representation of the server object.
277
- def inspect
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
- # Check whether the server connection is alive. This will cause the
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
- # Try to connect to the memcached server targeted by this object.
291
- # Returns the connected socket object on success or nil on failure.
292
- def socket
293
- # Attempt to connect if not already connected.
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
- # Close the connection to the memcached server targeted by this
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
- # Mark the server as dead and close its socket.
323
- def mark_dead(reason = "Unknown error")
324
- @sock.close if @sock && !@sock.closed?
325
- @sock = nil
326
- @retry = Time::now + RETRY_DELAY
327
-
328
- @status = sprintf("DEAD: %s, will retry at %s", reason, @retry)
329
- end
330
- end
331
-
332
-
333
- ###########################################################################
334
- # E X C E P T I O N C L A S S E S
335
- ###########################################################################
336
-
337
- # Base MemCache exception class.
338
- class MemCacheError < ::Exception
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
- # MemCache internal error class. Instances of this class mean that there
342
- # is some internal error either in the memcache client library or the
343
- # memcached server it is talking to.
344
- class InternalError < MemCacheError
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
- # MemCache client error class. Instances of this class mean that a
348
- # "CLIENT_ERROR" response was seen in the dialog with a memcached server.
349
- class ClientError < InternalError
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
- # MemCache server error class. Instances of this class mean that a
353
- # "SERVER_ERROR" response was seen in the dialog with a memcached server.
354
- class ServerError < InternalError
355
- attr_reader :server
356
-
357
- def initalize(server)
358
- @server = server
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
+
@@ -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.to_f
15
+ def self.get(key, expiry = 0)
16
+ start_time = Time.now
14
17
  result = CACHE.get key
15
- end_time = Time.now.to_f
16
- ActiveRecord::Base.logger.debug(
17
- sprintf("MemCache Get (%0.6f) %s",
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
- # MemCache error is a cache miss.
22
- ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
23
- return nil
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.to_f
38
+ start_time = Time.now
32
39
  CACHE.set key, value, expiry
33
- end_time = Time.now.to_f
34
- ActiveRecord::Base.logger.debug(
35
- sprintf("MemCache Set (%0.6f) %s",
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
- # Ignore put failure.
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.to_f
51
+ start_time = Time.now
47
52
  CACHE.delete key, delay
48
- end_time = Time.now.to_f
49
- ActiveRecord::Base.logger.debug(
50
- sprintf("MemCache Delete (%0.6f) %s",
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
- # Ignore delete failure.
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("MemCache Reset")
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.11.6
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.3
7
- date: 2006-01-17 00:00:00 -08:00
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
- email: bob@robotcoop.com
12
- homepage:
13
- rubyforge_project:
14
- description:
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: