memcache-client 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/Manifest.txt +5 -0
  2. data/README +44 -0
  3. data/Rakefile +55 -0
  4. data/lib/memcache.rb +361 -0
  5. data/lib/memcache_util.rb +68 -0
  6. metadata +49 -0
@@ -0,0 +1,5 @@
1
+ Manifest.txt
2
+ README
3
+ Rakefile
4
+ lib/memcache.rb
5
+ lib/memcache_util.rb
data/README ADDED
@@ -0,0 +1,44 @@
1
+ = memcache-client
2
+
3
+ Rubyforge Project:
4
+
5
+ http://rubyforge.org/projects/rctools/
6
+
7
+ == About
8
+
9
+ memcache-client is a fast memcached client.
10
+
11
+ == Installing memcache-client
12
+
13
+ Just install the gem
14
+
15
+ $ sudo gem install memcache-client
16
+
17
+ == Using memcache-client
18
+
19
+ CACHE = MemCache.new :c_threshold => 10_000,
20
+ :compression => true,
21
+ :debug => false,
22
+ :namespace => 'my_namespace',
23
+ :readonly => false,
24
+ :urlencode => false
25
+ CACHE.servers = 'localhost:11211'
26
+
27
+ You can specify multiple servers by sending an Array of servers to #servers=.
28
+
29
+ === Using memcache-client with Rails
30
+
31
+ Rails will automatically load the memcache-client gem, but you may wish to
32
+ uninstall Ruby-memcache, I don't know which one it will pick by default.
33
+
34
+ Add your environment-specific caches to config/environment/*. If you run both
35
+ development and production on the same machine be sure to use different
36
+ namespaces. Be careful when running tests under memcache, you may get strange
37
+ results, it will be less of a headache to simply use a readonly memcache when
38
+ testing.
39
+
40
+ memcache-client also comes with a wrapper called Cache in memcache_util.rb for
41
+ use with Rails. To use it be sure to assign your memcache connection to
42
+ CACHE. Cache returns nil on all memcache errors so you don't have to rescue
43
+ yourself. It has #get, #put and #delete module functions.
44
+
@@ -0,0 +1,55 @@
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 ]
23
+
24
+ Rake::TestTask.new('test') do |t|
25
+ t.libs << 'test'
26
+ t.pattern = 'test/test_*.rb'
27
+ t.verbose = true
28
+ end
29
+
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
34
+
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
42
+
43
+ desc 'Build Gem'
44
+ Rake::GemPackageTask.new spec do |pkg|
45
+ pkg.need_tar = true
46
+ end
47
+
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
55
+
@@ -0,0 +1,361 @@
1
+ require 'socket'
2
+ require 'thread'
3
+
4
+ # A Ruby client library for memcached.
5
+ #
6
+ # This is intended to provide access to basic memcached functionality. It
7
+ # 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.
12
+ 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
+
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)
58
+ end
59
+
60
+ # Returns whether there is at least one active server for the object.
61
+ def active?
62
+ not @servers.empty?
63
+ end
64
+
65
+ # Returns whether the cache was created read only.
66
+ def readonly?
67
+ @readonly
68
+ end
69
+
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
84
+ else
85
+ raise TypeError, "Cannot convert %s to MemCache::Server" %
86
+ svr.class.name
87
+ end
88
+ end
89
+
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
95
+ end
96
+
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)
102
+
103
+ sock = server.socket
104
+ if sock.nil?
105
+ raise MemCacheError, "No connection to server"
106
+ end
107
+
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
122
+
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
132
+
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
145
+
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
157
+
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)
164
+
165
+ sock = server.socket
166
+ if sock.nil?
167
+ raise MemCacheError, "No connection to server"
168
+ end
169
+
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
178
+ end
179
+
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
193
+
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
199
+
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}"
205
+ end
206
+
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
222
+
223
+ raise MemCacheError, "No servers available" unless server
224
+ server
225
+ end
226
+
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
266
+
267
+ @host = host
268
+ @port = port.to_i
269
+ @weight = weight.to_i
270
+
271
+ @sock = nil
272
+ @retry = nil
273
+ @status = "NOT CONNECTED"
274
+ end
275
+
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
281
+
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
289
+
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
312
+
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
321
+
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
339
+ end
340
+
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
345
+ end
346
+
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
350
+ end
351
+
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
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,68 @@
1
+ ##
2
+ # A utility wrapper around the MemCache client to simplify cache access. All
3
+ # methods silently ignore MemCache errors.
4
+
5
+ module Cache
6
+
7
+ ##
8
+ # Returns the object at +key+ from the cache if successful, or nil if
9
+ # either the object is not in the cache or if there was an error
10
+ # attermpting to access the cache.
11
+
12
+ def self.get(key)
13
+ start_time = Time.now.to_f
14
+ 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))
19
+ return result
20
+ rescue MemCache::MemCacheError => err
21
+ # MemCache error is a cache miss.
22
+ ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
23
+ return nil
24
+ end
25
+
26
+ ##
27
+ # Places +value+ in the cache at +key+, with an optional +expiry+ time in
28
+ # seconds. (?)
29
+
30
+ def self.put(key, value, expiry = 0)
31
+ start_time = Time.now.to_f
32
+ 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))
37
+ rescue MemCache::MemCacheError => err
38
+ # Ignore put failure.
39
+ ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
40
+ end
41
+
42
+ ##
43
+ # Deletes +key+ from the cache in +delay+ seconds. (?)
44
+
45
+ def self.delete(key, delay = nil)
46
+ start_time = Time.now.to_f
47
+ 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))
52
+ rescue MemCache::MemCacheError => err
53
+ # Ignore delete failure.
54
+ ActiveRecord::Base.logger.debug("MemCache Error: #{err.message}")
55
+ end
56
+
57
+ ##
58
+ # Resets all connections to mecache servers.
59
+
60
+ def self.reset
61
+ CACHE.reset
62
+ ActiveRecord::Base.logger.debug("MemCache Reset")
63
+ end
64
+
65
+ end
66
+
67
+ # vim: ts=4 sts=4 sw=4
68
+
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11.6
3
+ specification_version: 1
4
+ name: memcache-client
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.3
7
+ date: 2006-01-17 00:00:00 -08:00
8
+ summary: A Ruby memcached client
9
+ require_paths:
10
+ - lib
11
+ email: bob@robotcoop.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Robert Cottrell
30
+ files:
31
+ - Manifest.txt
32
+ - README
33
+ - Rakefile
34
+ - lib/memcache.rb
35
+ - lib/memcache_util.rb
36
+ test_files: []
37
+
38
+ rdoc_options: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ executables: []
43
+
44
+ extensions: []
45
+
46
+ requirements: []
47
+
48
+ dependencies: []
49
+