memcache-client 1.0.3

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.
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
+