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.
@@ -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: