Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 91b9c2ceaa07ff5bfa1a4f513b0af2225d22231b 0 parents
@mperham mperham authored
68 History.txt
@@ -0,0 +1,68 @@
+= 1.5.0
+
+* Add MemCache#flush_all command. Patch #13019 and bug #10503. Patches
+ submitted by Sebastian Delmont and Rick Olson.
+* Type-cast data returned by MemCache#stats. Patch #10505 submitted by
+ Sebastian Delmont.
+
+= 1.4.0
+
+* Fix bug #10371, #set does not check response for server errors.
+ Submitted by Ben VandenBos.
+* Fix bug #12450, set TCP_NODELAY socket option. Patch by Chris
+ McGrath.
+* Fix bug #10704, missing #add method. Patch by Jamie Macey.
+* Fix bug #10371, handle socket EOF in cache_get. Submitted by Ben
+ VandenBos.
+
+= 1.3.0
+
+* Apply patch #6507, add stats command. Submitted by Tyler Kovacs.
+* Apply patch #6509, parallel implementation of #get_multi. Submitted
+ by Tyler Kovacs.
+* Validate keys. Disallow spaces in keys or keys that are too long.
+* Perform more validation of server responses. MemCache now reports
+ errors if the socket was not in an expected state. (Please file
+ bugs if you find some.)
+* Add #incr and #decr.
+* Add raw argument to #set and #get to retrieve #incr and #decr
+ values.
+* Also put on MemCacheError when using Cache::get with block.
+* memcache.rb no longer sets $TESTING to a true value if it was
+ previously defined. Bug #8213 by Matijs van Zuijlen.
+
+= 1.2.1
+
+* Fix bug #7048, MemCache#servers= referenced changed local variable.
+ Submitted by Justin Dossey.
+* Fix bug #7049, MemCache#initialize resets @buckets. Submitted by
+ Justin Dossey.
+* Fix bug #6232, Make Cache::Get work with a block only when nil is
+ returned. Submitted by Jon Evans.
+* Moved to the seattlerb project.
+
+= 1.2.0
+
+NOTE: This version will store keys in different places than previous
+versions! Be prepared for some thrashing while memcached sorts itself
+out!
+
+* Fixed multithreaded operations, bug 5994 and 5989.
+ Thanks to Blaine Cook, Erik Hetzner, Elliot Smith, Dave Myron (and
+ possibly others I have forgotten).
+* Made memcached interoperable with other memcached libraries, bug
+ 4509. Thanks to anonymous.
+* Added get_multi to match Perl/etc APIs
+
+= 1.1.0
+
+* Added some tests
+* Sped up non-multithreaded and multithreaded operation
+* More Ruby-memcache compatibility
+* More RDoc
+* Switched to Hoe
+
+= 1.0.0
+
+Birthday!
+
28 LICENSE.txt
@@ -0,0 +1,28 @@
+All original code copyright 2005, 2006, 2007 Bob Cottrell, Eric Hodel,
+The Robot Co-op. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the names of the authors nor the names of their contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
8 Manifest.txt
@@ -0,0 +1,8 @@
+History.txt
+LICENSE.txt
+Manifest.txt
+README.txt
+Rakefile
+lib/memcache.rb
+lib/memcache_util.rb
+test/test_mem_cache.rb
54 README.txt
@@ -0,0 +1,54 @@
+= memcache-client
+
+Rubyforge Project:
+
+http://rubyforge.org/projects/seattlerb
+
+File bugs:
+
+http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
+
+Documentation:
+
+http://seattlerb.org/memcache-client
+
+== About
+
+memcache-client is a client for Danga Interactive's memcached.
+
+== Installing memcache-client
+
+Just install the gem:
+
+ $ sudo gem install memcache-client
+
+== Using memcache-client
+
+With one server:
+
+ CACHE = MemCache.new 'localhost:11211', :namespace => 'my_namespace'
+
+Or with multiple servers:
+
+ CACHE = MemCache.new %w[one.example.com:11211 two.example.com:11211],
+ :namespace => 'my_namespace'
+
+See MemCache.new for details.
+
+=== Using memcache-client with Rails
+
+Rails will automatically load the memcache-client gem, but you may
+need to uninstall Ruby-memcache, I don't know which one will get
+picked by default.
+
+Add your environment-specific caches to config/environment/*. If you run both
+development and production on the same memcached server sets, be sure
+to use different namespaces. Be careful when running tests using
+memcache, you may get strange results. It will be less of a headache
+to simply use a readonly memcache when testing.
+
+memcache-client also comes with a wrapper called Cache in memcache_util.rb for
+use with Rails. To use it be sure to assign your memcache connection to
+CACHE. Cache returns nil on all memcache errors so you don't have to rescue
+the errors yourself. It has #get, #put and #delete module functions.
+
19 Rakefile
@@ -0,0 +1,19 @@
+# vim: syntax=Ruby
+
+require 'hoe'
+
+$:.unshift 'lib'
+require 'memcache'
+
+hoe = Hoe.new 'memcache-client', MemCache::VERSION do |p|
+ p.summary = 'A Ruby memcached client'
+ p.description = p.paragraphs_of('README.txt', 8).first
+ p.author = ['Eric Hodel', 'Robert Cottrell']
+ p.email = 'drbrain@segment7.net'
+ p.url = p.paragraphs_of('README.txt', 6).first
+ p.changes = File.read('History.txt').scan(/\A(=.*?)^=/m).first.first
+
+ p.rubyforge_name = 'seattlerb'
+ p.extra_deps << ['ZenTest', '>= 3.4.2']
+end
+
805 lib/memcache.rb
@@ -0,0 +1,805 @@
+$TESTING = defined?($TESTING) && $TESTING
+
+require 'socket'
+require 'thread'
+require 'timeout'
+require 'rubygems'
+
+class String
+
+ ##
+ # Uses the ITU-T polynomial in the CRC32 algorithm.
+
+ def crc32_ITU_T
+ n = length
+ r = 0xFFFFFFFF
+
+ n.times do |i|
+ r ^= self[i]
+ 8.times do
+ if (r & 1) != 0 then
+ r = (r>>1) ^ 0xEDB88320
+ else
+ r >>= 1
+ end
+ end
+ end
+
+ r ^ 0xFFFFFFFF
+ end
+
+end
+
+##
+# A Ruby client library for memcached.
+#
+# This is intended to provide access to basic memcached functionality. It
+# does not attempt to be complete implementation of the entire API, but it is
+# approaching a complete implementation.
+
+class MemCache
+
+ ##
+ # The version of MemCache you are using.
+
+ VERSION = '1.5.0'
+
+ ##
+ # Default options for the cache object.
+
+ DEFAULT_OPTIONS = {
+ :namespace => nil,
+ :readonly => false,
+ :multithread => false,
+ }
+
+ ##
+ # Default memcached port.
+
+ DEFAULT_PORT = 11211
+
+ ##
+ # Default memcached server weight.
+
+ DEFAULT_WEIGHT = 1
+
+ ##
+ # The amount of time to wait for a response from a memcached server. If a
+ # response is not completed within this time, the connection to the server
+ # will be closed and an error will be raised.
+
+ attr_accessor :request_timeout
+
+ ##
+ # The namespace for this instance
+
+ attr_reader :namespace
+
+ ##
+ # The multithread setting for this instance
+
+ attr_reader :multithread
+
+ ##
+ # The servers this client talks to. Play at your own peril.
+
+ attr_reader :servers
+
+ ##
+ # Accepts a list of +servers+ and a list of +opts+. +servers+ may be
+ # omitted. See +servers=+ for acceptable server list arguments.
+ #
+ # Valid options for +opts+ are:
+ #
+ # [:namespace] Prepends this value to all keys added or retrieved.
+ # [:readonly] Raises an exeception on cache writes when true.
+ # [:multithread] Wraps cache access in a Mutex for thread safety.
+ #
+ # Other options are ignored.
+
+ def initialize(*args)
+ servers = []
+ opts = {}
+
+ case args.length
+ when 0 then # NOP
+ when 1 then
+ arg = args.shift
+ case arg
+ when Hash then opts = arg
+ when Array then servers = arg
+ when String then servers = [arg]
+ else raise ArgumentError, 'first argument must be Array, Hash or String'
+ end
+ when 2 then
+ servers, opts = args
+ else
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 2)"
+ end
+
+ opts = DEFAULT_OPTIONS.merge opts
+ @namespace = opts[:namespace]
+ @readonly = opts[:readonly]
+ @multithread = opts[:multithread]
+ @mutex = Mutex.new if @multithread
+ @buckets = []
+ self.servers = servers
+ end
+
+ ##
+ # Returns a string representation of the cache object.
+
+ def inspect
+ "<MemCache: %d servers, %d buckets, ns: %p, ro: %p>" %
+ [@servers.length, @buckets.length, @namespace, @readonly]
+ end
+
+ ##
+ # Returns whether there is at least one active server for the object.
+
+ def active?
+ not @servers.empty?
+ end
+
+ ##
+ # Returns whether or not the cache object was created read only.
+
+ def readonly?
+ @readonly
+ end
+
+ ##
+ # Set the servers that the requests will be distributed between. Entries
+ # can be either strings of the form "hostname:port" or
+ # "hostname:port:weight" or MemCache::Server objects.
+
+ def servers=(servers)
+ # Create the server objects.
+ @servers = servers.collect do |server|
+ case server
+ when String
+ host, port, weight = server.split ':', 3
+ port ||= DEFAULT_PORT
+ weight ||= DEFAULT_WEIGHT
+ Server.new self, host, port, weight
+ when Server
+ if server.memcache.multithread != @multithread then
+ raise ArgumentError, "can't mix threaded and non-threaded servers"
+ end
+ server
+ else
+ raise TypeError, "cannot convert #{server.class} into MemCache::Server"
+ end
+ end
+
+ # Create an array of server buckets for weight selection of servers.
+ @buckets = []
+ @servers.each do |server|
+ server.weight.times { @buckets.push(server) }
+ end
+ end
+
+ ##
+ # Deceremets the value for +key+ by +amount+ and returns the new value.
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
+ # 0. +key+ can not be decremented below 0.
+
+ def decr(key, amount = 1)
+ server, cache_key = request_setup key
+
+ if @multithread then
+ threadsafe_cache_decr server, cache_key, amount
+ else
+ cache_decr server, cache_key, amount
+ end
+ rescue TypeError, SocketError, SystemCallError, IOError => err
+ handle_error server, err
+ end
+
+ ##
+ # Retrieves +key+ from memcache. If +raw+ is false, the value will be
+ # unmarshalled.
+
+ def get(key, raw = false)
+ server, cache_key = request_setup key
+
+ value = if @multithread then
+ threadsafe_cache_get server, cache_key
+ else
+ cache_get server, cache_key
+ end
+
+ return nil if value.nil?
+
+ value = Marshal.load value unless raw
+
+ return value
+ rescue TypeError, SocketError, SystemCallError, IOError => err
+ handle_error server, err
+ end
+
+ ##
+ # Retrieves multiple values from memcached in parallel, if possible.
+ #
+ # The memcached protocol supports the ability to retrieve multiple
+ # keys in a single request. Pass in an array of keys to this method
+ # and it will:
+ #
+ # 1. map the key to the appropriate memcached server
+ # 2. send a single request to each server that has one or more key values
+ #
+ # Returns a hash of values.
+ #
+ # cache["a"] = 1
+ # cache["b"] = 2
+ # cache.get_multi "a", "b" # => { "a" => 1, "b" => 2 }
+
+ def get_multi(*keys)
+ raise MemCacheError, 'No active servers' unless active?
+
+ keys.flatten!
+ key_count = keys.length
+ cache_keys = {}
+ server_keys = Hash.new { |h,k| h[k] = [] }
+
+ # map keys to servers
+ keys.each do |key|
+ server, cache_key = request_setup key
+ cache_keys[cache_key] = key
+ server_keys[server] << cache_key
+ end
+
+ results = {}
+
+ server_keys.each do |server, keys|
+ keys = keys.join ' '
+ values = if @multithread then
+ threadsafe_cache_get_multi server, keys
+ else
+ cache_get_multi server, keys
+ end
+ values.each do |key, value|
+ results[cache_keys[key]] = Marshal.load value
+ end
+ end
+
+ return results
+ rescue TypeError, SocketError, SystemCallError, IOError => err
+ handle_error server, err
+ end
+
+ ##
+ # Increments the value for +key+ by +amount+ and retruns the new value.
+ # +key+ must already exist. If +key+ is not an integer, it is assumed to be
+ # 0.
+
+ def incr(key, amount = 1)
+ server, cache_key = request_setup key
+
+ if @multithread then
+ threadsafe_cache_incr server, cache_key, amount
+ else
+ cache_incr server, cache_key, amount
+ end
+ rescue TypeError, SocketError, SystemCallError, IOError => err
+ handle_error server, err
+ end
+
+ ##
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
+ # seconds. If +raw+ is true, +value+ will not be Marshalled.
+ #
+ # Warning: Readers should not call this method in the event of a cache miss;
+ # see MemCache#add.
+
+ def set(key, value, expiry = 0, raw = false)
+ raise MemCacheError, "Update of readonly cache" if @readonly
+ server, cache_key = request_setup key
+ socket = server.socket
+
+ value = Marshal.dump value unless raw
+ command = "set #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
+
+ begin
+ @mutex.lock if @multithread
+ socket.write command
+ result = socket.gets
+ raise MemCacheError, $1.strip if result =~ /^SERVER_ERROR (.*)/
+ rescue SocketError, SystemCallError, IOError => err
+ server.close
+ raise MemCacheError, err.message
+ ensure
+ @mutex.unlock if @multithread
+ end
+ end
+
+ ##
+ # Add +key+ to the cache with value +value+ that expires in +expiry+
+ # seconds, but only if +key+ does not already exist in the cache.
+ # If +raw+ is true, +value+ will not be Marshalled.
+ #
+ # Readers should call this method in the event of a cache miss, not
+ # MemCache#set or MemCache#[]=.
+
+ def add(key, value, expiry = 0, raw = false)
+ raise MemCacheError, "Update of readonly cache" if @readonly
+ server, cache_key = request_setup key
+ socket = server.socket
+
+ value = Marshal.dump value unless raw
+ command = "add #{cache_key} 0 #{expiry} #{value.size}\r\n#{value}\r\n"
+
+ begin
+ @mutex.lock if @multithread
+ socket.write command
+ socket.gets
+ rescue SocketError, SystemCallError, IOError => err
+ server.close
+ raise MemCacheError, err.message
+ ensure
+ @mutex.unlock if @multithread
+ end
+ end
+
+ ##
+ # Removes +key+ from the cache in +expiry+ seconds.
+
+ def delete(key, expiry = 0)
+ @mutex.lock if @multithread
+
+ raise MemCacheError, "No active servers" unless active?
+ cache_key = make_cache_key key
+ server = get_server_for_key cache_key
+
+ sock = server.socket
+ raise MemCacheError, "No connection to server" if sock.nil?
+
+ begin
+ sock.write "delete #{cache_key} #{expiry}\r\n"
+ sock.gets
+ rescue SocketError, SystemCallError, IOError => err
+ server.close
+ raise MemCacheError, err.message
+ end
+ ensure
+ @mutex.unlock if @multithread
+ end
+
+ ##
+ # Flush the cache from all memcache servers.
+
+ def flush_all
+ raise MemCacheError, 'No active servers' unless active?
+ raise MemCacheError, "Update of readonly cache" if @readonly
+ begin
+ @mutex.lock if @multithread
+ @servers.each do |server|
+ begin
+ sock = server.socket
+ raise MemCacheError, "No connection to server" if sock.nil?
+ sock.write "flush_all\r\n"
+ result = sock.gets
+ raise MemCacheError, $2.strip if result =~ /^(SERVER_)?ERROR(.*)/
+ rescue SocketError, SystemCallError, IOError => err
+ server.close
+ raise MemCacheError, err.message
+ end
+ end
+ ensure
+ @mutex.unlock if @multithread
+ end
+ end
+
+ ##
+ # Reset the connection to all memcache servers. This should be called if
+ # there is a problem with a cache lookup that might have left the connection
+ # in a corrupted state.
+
+ def reset
+ @servers.each { |server| server.close }
+ end
+
+ ##
+ # Returns statistics for each memcached server. An explanation of the
+ # statistics can be found in the memcached docs:
+ #
+ # http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt
+ #
+ # Example:
+ #
+ # >> pp CACHE.stats
+ # {"localhost:11211"=>
+ # {"bytes"=>4718,
+ # "pid"=>20188,
+ # "connection_structures"=>4,
+ # "time"=>1162278121,
+ # "pointer_size"=>32,
+ # "limit_maxbytes"=>67108864,
+ # "cmd_get"=>14532,
+ # "version"=>"1.2.0",
+ # "bytes_written"=>432583,
+ # "cmd_set"=>32,
+ # "get_misses"=>0,
+ # "total_connections"=>19,
+ # "curr_connections"=>3,
+ # "curr_items"=>4,
+ # "uptime"=>1557,
+ # "get_hits"=>14532,
+ # "total_items"=>32,
+ # "rusage_system"=>0.313952,
+ # "rusage_user"=>0.119981,
+ # "bytes_read"=>190619}}
+ # => nil
+
+ def stats
+ raise MemCacheError, "No active servers" unless active?
+ server_stats = {}
+
+ @servers.each do |server|
+ sock = server.socket
+ raise MemCacheError, "No connection to server" if sock.nil?
+
+ value = nil
+ begin
+ sock.write "stats\r\n"
+ stats = {}
+ while line = sock.gets do
+ break if line == "END\r\n"
+ if line =~ /^STAT ([\w]+) ([\w\.\:]+)/ then
+ name, value = $1, $2
+ stats[name] = case name
+ when 'version'
+ value
+ when 'rusage_user', 'rusage_system' then
+ seconds, microseconds = value.split(/:/, 2)
+ microseconds ||= 0
+ Float(seconds) + (Float(microseconds) / 1_000_000)
+ else
+ if value =~ /^\d+$/ then
+ value.to_i
+ else
+ value
+ end
+ end
+ end
+ end
+ server_stats["#{server.host}:#{server.port}"] = stats
+ rescue SocketError, SystemCallError, IOError => err
+ server.close
+ raise MemCacheError, err.message
+ end
+ end
+
+ server_stats
+ end
+
+ ##
+ # Shortcut to get a value from the cache.
+
+ alias [] get
+
+ ##
+ # Shortcut to save a value in the cache. This method does not set an
+ # expiration on the entry. Use set to specify an explicit expiry.
+
+ def []=(key, value)
+ set key, value
+ end
+
+ protected unless $TESTING
+
+ ##
+ # Create a key for the cache, incorporating the namespace qualifier if
+ # requested.
+
+ def make_cache_key(key)
+ if namespace.nil? then
+ key
+ else
+ "#{@namespace}:#{key}"
+ end
+ end
+
+ ##
+ # Pick a server to handle the request based on a hash of the key.
+
+ def get_server_for_key(key)
+ raise ArgumentError, "illegal character in key #{key.inspect}" if
+ key =~ /\s/
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
+ raise MemCacheError, "No servers available" if @servers.empty?
+ return @servers.first if @servers.length == 1
+
+ hkey = hash_for key
+
+ 20.times do |try|
+ server = @buckets[hkey % @buckets.nitems]
+ return server if server.alive?
+ hkey += hash_for "#{try}#{key}"
+ end
+
+ raise MemCacheError, "No servers available"
+ end
+
+ ##
+ # Returns an interoperable hash value for +key+. (I think, docs are
+ # sketchy for down servers).
+
+ def hash_for(key)
+ (key.crc32_ITU_T >> 16) & 0x7fff
+ end
+
+ ##
+ # Performs a raw decr for +cache_key+ from +server+. Returns nil if not
+ # found.
+
+ def cache_decr(server, cache_key, amount)
+ socket = server.socket
+ socket.write "decr #{cache_key} #{amount}\r\n"
+ text = socket.gets
+ return nil if text == "NOT_FOUND\r\n"
+ return text.to_i
+ end
+
+ ##
+ # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache
+ # miss.
+
+ def cache_get(server, cache_key)
+ socket = server.socket
+ socket.write "get #{cache_key}\r\n"
+ keyline = socket.gets # "VALUE <key> <flags> <bytes>\r\n"
+
+ if keyline.nil? then
+ server.close
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
+ end
+
+ return nil if keyline == "END\r\n"
+
+ unless keyline =~ /(\d+)\r/ then
+ server.close
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
+ end
+ value = socket.read $1.to_i
+ socket.read 2 # "\r\n"
+ socket.gets # "END\r\n"
+ return value
+ end
+
+ ##
+ # Fetches +cache_keys+ from +server+ using a multi-get.
+
+ def cache_get_multi(server, cache_keys)
+ values = {}
+ socket = server.socket
+ socket.write "get #{cache_keys}\r\n"
+
+ while keyline = socket.gets do
+ return values if keyline == "END\r\n"
+
+ unless keyline =~ /^VALUE (.+) (.+) (.+)/ then
+ server.close
+ raise MemCacheError, "unexpected response #{keyline.inspect}"
+ end
+
+ key, data_length = $1, $3
+ values[$1] = socket.read data_length.to_i
+ socket.read(2) # "\r\n"
+ end
+
+ server.close
+ raise MemCacheError, "lost connection to #{server.host}:#{server.port}"
+ end
+
+ ##
+ # Performs a raw incr for +cache_key+ from +server+. Returns nil if not
+ # found.
+
+ def cache_incr(server, cache_key, amount)
+ socket = server.socket
+ socket.write "incr #{cache_key} #{amount}\r\n"
+ text = socket.gets
+ return nil if text == "NOT_FOUND\r\n"
+ return text.to_i
+ end
+
+ ##
+ # Handles +error+ from +server+.
+
+ def handle_error(server, error)
+ server.close if server
+ new_error = MemCacheError.new error.message
+ new_error.set_backtrace error.backtrace
+ raise new_error
+ end
+
+ ##
+ # Performs setup for making a request with +key+ from memcached. Returns
+ # the server to fetch the key from and the complete key to use.
+
+ def request_setup(key)
+ raise MemCacheError, 'No active servers' unless active?
+ cache_key = make_cache_key key
+ server = get_server_for_key cache_key
+ raise MemCacheError, 'No connection to server' if server.socket.nil?
+ return server, cache_key
+ end
+
+ def threadsafe_cache_decr(server, cache_key, amount) # :nodoc:
+ @mutex.lock
+ cache_decr server, cache_key, amount
+ ensure
+ @mutex.unlock
+ end
+
+ def threadsafe_cache_get(server, cache_key) # :nodoc:
+ @mutex.lock
+ cache_get server, cache_key
+ ensure
+ @mutex.unlock
+ end
+
+ def threadsafe_cache_get_multi(socket, cache_keys) # :nodoc:
+ @mutex.lock
+ cache_get_multi socket, cache_keys
+ ensure
+ @mutex.unlock
+ end
+
+ def threadsafe_cache_incr(server, cache_key, amount) # :nodoc:
+ @mutex.lock
+ cache_incr server, cache_key, amount
+ ensure
+ @mutex.unlock
+ end
+
+ ##
+ # This class represents a memcached server instance.
+
+ class Server
+
+ ##
+ # The amount of time to wait to establish a connection with a memcached
+ # server. If a connection cannot be established within this time limit,
+ # the server will be marked as down.
+
+ CONNECT_TIMEOUT = 0.25
+
+ ##
+ # The amount of time to wait before attempting to re-establish a
+ # connection with a server that is marked dead.
+
+ RETRY_DELAY = 30.0
+
+ ##
+ # The host the memcached server is running on.
+
+ attr_reader :host
+
+ ##
+ # The port the memcached server is listening on.
+
+ attr_reader :port
+
+ ##
+ # The weight given to the server.
+
+ attr_reader :weight
+
+ ##
+ # The time of next retry if the connection is dead.
+
+ attr_reader :retry
+
+ ##
+ # A text status string describing the state of the server.
+
+ attr_reader :status
+
+ ##
+ # Create a new MemCache::Server object for the memcached instance
+ # listening on the given host and port, weighted by the given weight.
+
+ def initialize(memcache, host, port = DEFAULT_PORT, weight = DEFAULT_WEIGHT)
+ raise ArgumentError, "No host specified" if host.nil? or host.empty?
+ raise ArgumentError, "No port specified" if port.nil? or port.to_i.zero?
+
+ @memcache = memcache
+ @host = host
+ @port = port.to_i
+ @weight = weight.to_i
+
+ @multithread = @memcache.multithread
+ @mutex = Mutex.new
+
+ @sock = nil
+ @retry = nil
+ @status = 'NOT CONNECTED'
+ end
+
+ ##
+ # Return a string representation of the server object.
+
+ def inspect
+ "<MemCache::Server: %s:%d [%d] (%s)>" % [@host, @port, @weight, @status]
+ end
+
+ ##
+ # Check whether the server connection is alive. This will cause the
+ # socket to attempt to connect if it isn't already connected and or if
+ # the server was previously marked as down and the retry time has
+ # been exceeded.
+
+ def alive?
+ !!socket
+ end
+
+ ##
+ # Try to connect to the memcached server targeted by this object.
+ # Returns the connected socket object on success or nil on failure.
+
+ def socket
+ @mutex.lock if @multithread
+ return @sock if @sock and not @sock.closed?
+
+ @sock = nil
+
+ # If the host was dead, don't retry for a while.
+ return if @retry and @retry > Time.now
+
+ # Attempt to connect if not already connected.
+ begin
+ @sock = timeout CONNECT_TIMEOUT do
+ TCPSocket.new @host, @port
+ end
+ if Socket.constants.include? 'TCP_NODELAY' then
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
+ end
+ @retry = nil
+ @status = 'CONNECTED'
+ rescue SocketError, SystemCallError, IOError, Timeout::Error => err
+ mark_dead err.message
+ end
+
+ return @sock
+ ensure
+ @mutex.unlock if @multithread
+ end
+
+ ##
+ # Close the connection to the memcached server targeted by this
+ # object. The server is not considered dead.
+
+ def close
+ @mutex.lock if @multithread
+ @sock.close if @sock && !@sock.closed?
+ @sock = nil
+ @retry = nil
+ @status = "NOT CONNECTED"
+ ensure
+ @mutex.unlock if @multithread
+ end
+
+ private
+
+ ##
+ # Mark the server as dead and close its socket.
+
+ def mark_dead(reason = "Unknown error")
+ @sock.close if @sock && !@sock.closed?
+ @sock = nil
+ @retry = Time.now + RETRY_DELAY
+
+ @status = sprintf "DEAD: %s, will retry at %s", reason, @retry
+ end
+
+ end
+
+ ##
+ # Base MemCache exception class.
+
+ class MemCacheError < RuntimeError; end
+
+end
+
90 lib/memcache_util.rb
@@ -0,0 +1,90 @@
+##
+# A utility wrapper around the MemCache client to simplify cache access. All
+# methods silently ignore MemCache errors.
+
+module Cache
+
+ ##
+ # Returns the object at +key+ from the cache if successful, or nil if either
+ # the object is not in the cache or if there was an error attermpting to
+ # access the cache.
+ #
+ # If there is a cache miss and a block is given the result of the block will
+ # be stored in the cache with optional +expiry+, using the +add+ method rather
+ # than +set+.
+
+ def self.get(key, expiry = 0)
+ start_time = Time.now
+ value = CACHE.get key
+ elapsed = Time.now - start_time
+ ActiveRecord::Base.logger.debug('MemCache Get (%0.6f) %s' % [elapsed, key])
+ if value.nil? and block_given? then
+ value = yield
+ add key, value, expiry
+ end
+ value
+ rescue MemCache::MemCacheError => err
+ ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
+ if block_given? then
+ value = yield
+ put key, value, expiry
+ end
+ value
+ end
+
+ ##
+ # Sets +value+ in the cache at +key+, with an optional +expiry+ time in
+ # seconds.
+
+ def self.put(key, value, expiry = 0)
+ start_time = Time.now
+ CACHE.set key, value, expiry
+ elapsed = Time.now - start_time
+ ActiveRecord::Base.logger.debug('MemCache Set (%0.6f) %s' % [elapsed, key])
+ value
+ rescue MemCache::MemCacheError => err
+ ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
+ nil
+ end
+
+ ##
+ # Sets +value+ in the cache at +key+, with an optional +expiry+ time in
+ # seconds. If +key+ already exists in cache, returns nil.
+
+ def self.add(key, value, expiry = 0)
+ start_time = Time.now
+ response = CACHE.add key, value, expiry
+ elapsed = Time.now - start_time
+ ActiveRecord::Base.logger.debug('MemCache Add (%0.6f) %s' % [elapsed, key])
+ (response == "STORED\r\n") ? value : nil
+ rescue MemCache::MemCacheError => err
+ ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
+ nil
+ end
+
+ ##
+ # Deletes +key+ from the cache in +delay+ seconds.
+
+ def self.delete(key, delay = nil)
+ start_time = Time.now
+ CACHE.delete key, delay
+ elapsed = Time.now - start_time
+ ActiveRecord::Base.logger.debug('MemCache Delete (%0.6f) %s' %
+ [elapsed, key])
+ nil
+ rescue MemCache::MemCacheError => err
+ ActiveRecord::Base.logger.debug "MemCache Error: #{err.message}"
+ nil
+ end
+
+ ##
+ # Resets all connections to MemCache servers.
+
+ def self.reset
+ CACHE.reset
+ ActiveRecord::Base.logger.debug 'MemCache Connections Reset'
+ nil
+ end
+
+end
+
744 test/test_mem_cache.rb
@@ -0,0 +1,744 @@
+require 'stringio'
+require 'test/unit'
+require 'rubygems'
+require 'test/zentest_assertions'
+
+$TESTING = true
+
+require 'memcache'
+
+class MemCache
+
+ attr_writer :namespace
+
+end
+
+class FakeSocket
+
+ attr_reader :written, :data
+
+ def initialize
+ @written = StringIO.new
+ @data = StringIO.new
+ end
+
+ def write(data)
+ @written.write data
+ end
+
+ def gets
+ @data.gets
+ end
+
+ def read(arg)
+ @data.read arg
+ end
+
+end
+
+class FakeServer
+
+ attr_reader :host, :port, :socket
+
+ def initialize(socket = nil)
+ @closed = false
+ @host = 'example.com'
+ @port = 11211
+ @socket = socket || FakeSocket.new
+ end
+
+ def close
+ @closed = true
+ end
+
+ def alive?
+ !@closed
+ end
+
+end
+
+class TestMemCache < Test::Unit::TestCase
+
+ def setup
+ @cache = MemCache.new 'localhost:1', :namespace => 'my_namespace'
+ end
+
+ def test_cache_get
+ server = util_setup_fake_server
+
+ assert_equal "\004\b\"\0170123456789",
+ @cache.cache_get(server, 'my_namespace:key')
+
+ assert_equal "get my_namespace:key\r\n",
+ server.socket.written.string
+ end
+
+ def test_cache_get_EOF
+ server = util_setup_fake_server
+ server.socket.data.string = ''
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.cache_get server, 'my_namespace:key'
+ end
+
+ assert_equal "lost connection to example.com:11211", e.message
+ end
+
+ def test_cache_get_bad_state
+ server = FakeServer.new
+ server.socket.data.write "bogus response\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.cache_get(server, 'my_namespace:key')
+ end
+
+ assert_equal "unexpected response \"bogus response\\r\\n\"", e.message
+
+ deny server.alive?
+
+ assert_equal "get my_namespace:key\r\n",
+ server.socket.written.string
+ end
+
+ def test_cache_get_miss
+ socket = FakeSocket.new
+ socket.data.write "END\r\n"
+ socket.data.rewind
+ server = FakeServer.new socket
+
+ assert_equal nil, @cache.cache_get(server, 'my_namespace:key')
+
+ assert_equal "get my_namespace:key\r\n",
+ socket.written.string
+ end
+
+ def test_cache_get_multi
+ server = util_setup_fake_server
+ server.socket.data.write "VALUE foo 0 7\r\n"
+ server.socket.data.write "\004\b\"\bfoo\r\n"
+ server.socket.data.write "VALUE bar 0 7\r\n"
+ server.socket.data.write "\004\b\"\bbar\r\n"
+ server.socket.data.write "END\r\n"
+ server.socket.data.rewind
+
+ result = @cache.cache_get_multi server, 'foo bar baz'
+
+ assert_equal 2, result.length
+ assert_equal "\004\b\"\bfoo", result['foo']
+ assert_equal "\004\b\"\bbar", result['bar']
+ end
+
+ def test_cache_get_multi_EOF
+ server = util_setup_fake_server
+ server.socket.data.string = ''
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.cache_get_multi server, 'my_namespace:key'
+ end
+
+ assert_equal "lost connection to example.com:11211", e.message
+ end
+
+ def test_cache_get_multi_bad_state
+ server = FakeServer.new
+ server.socket.data.write "bogus response\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.cache_get_multi server, 'my_namespace:key'
+ end
+
+ assert_equal "unexpected response \"bogus response\\r\\n\"", e.message
+
+ deny server.alive?
+
+ assert_equal "get my_namespace:key\r\n",
+ server.socket.written.string
+ end
+
+ def test_crc32_ITU_T
+ assert_equal 0, ''.crc32_ITU_T
+ assert_equal 1260851911, 'my_namespace:key'.crc32_ITU_T
+ end
+
+ def test_initialize
+ cache = MemCache.new :namespace => 'my_namespace', :readonly => true
+
+ assert_equal 'my_namespace', cache.namespace
+ assert_equal true, cache.readonly?
+ assert_equal true, cache.servers.empty?
+ end
+
+ def test_initialize_compatible
+ cache = MemCache.new ['localhost:11211', 'localhost:11212'],
+ :namespace => 'my_namespace', :readonly => true
+
+ assert_equal 'my_namespace', cache.namespace
+ assert_equal true, cache.readonly?
+ assert_equal false, cache.servers.empty?
+ end
+
+ def test_initialize_compatible_no_hash
+ cache = MemCache.new ['localhost:11211', 'localhost:11212']
+
+ assert_equal nil, cache.namespace
+ assert_equal false, cache.readonly?
+ assert_equal false, cache.servers.empty?
+ end
+
+ def test_initialize_compatible_one_server
+ cache = MemCache.new 'localhost:11211'
+
+ assert_equal nil, cache.namespace
+ assert_equal false, cache.readonly?
+ assert_equal false, cache.servers.empty?
+ end
+
+ def test_initialize_compatible_bad_arg
+ e = assert_raise ArgumentError do
+ cache = MemCache.new Object.new
+ end
+
+ assert_equal 'first argument must be Array, Hash or String', e.message
+ end
+
+ def test_initialize_multiple_servers
+ cache = MemCache.new %w[localhost:11211 localhost:11212],
+ :namespace => 'my_namespace', :readonly => true
+
+ assert_equal 'my_namespace', cache.namespace
+ assert_equal true, cache.readonly?
+ assert_equal false, cache.servers.empty?
+ deny_empty cache.instance_variable_get(:@buckets)
+ end
+
+ def test_initialize_too_many_args
+ assert_raises ArgumentError do
+ MemCache.new 1, 2, 3
+ end
+ end
+
+ def test_decr
+ server = FakeServer.new
+ server.socket.data.write "5\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.decr 'key'
+
+ assert_equal "decr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal 5, value
+ end
+
+ def test_decr_not_found
+ server = FakeServer.new
+ server.socket.data.write "NOT_FOUND\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.decr 'key'
+
+ assert_equal "decr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal nil, value
+ end
+
+ def test_decr_space_padding
+ server = FakeServer.new
+ server.socket.data.write "5 \r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.decr 'key'
+
+ assert_equal "decr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal 5, value
+ end
+
+ def test_get
+ util_setup_fake_server
+
+ value = @cache.get 'key'
+
+ assert_equal "get my_namespace:key\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal '0123456789', value
+ end
+
+ def test_get_bad_key
+ util_setup_fake_server
+ assert_raise ArgumentError do @cache.get 'k y' end
+
+ util_setup_fake_server
+ assert_raise ArgumentError do @cache.get 'k' * 250 end
+ end
+
+ def test_get_cache_get_IOError
+ socket = Object.new
+ def socket.write(arg) raise IOError, 'some io error'; end
+ server = FakeServer.new socket
+
+ @cache.servers = []
+ @cache.servers << server
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.get 'my_namespace:key'
+ end
+
+ assert_equal 'some io error', e.message
+ end
+
+ def test_get_cache_get_SystemCallError
+ socket = Object.new
+ def socket.write(arg) raise SystemCallError, 'some syscall error'; end
+ server = FakeServer.new socket
+
+ @cache.servers = []
+ @cache.servers << server
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.get 'my_namespace:key'
+ end
+
+ assert_equal 'unknown error - some syscall error', e.message
+ end
+
+ def test_get_no_connection
+ @cache.servers = 'localhost:1'
+ e = assert_raise MemCache::MemCacheError do
+ @cache.get 'key'
+ end
+
+ assert_equal 'No connection to server', e.message
+ end
+
+ def test_get_no_servers
+ @cache.servers = []
+ e = assert_raise MemCache::MemCacheError do
+ @cache.get 'key'
+ end
+
+ assert_equal 'No active servers', e.message
+ end
+
+ def test_get_multi
+ server = FakeServer.new
+ server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
+ server.socket.data.write "\004\b\"\0170123456789\r\n"
+ server.socket.data.write "VALUE my_namespace:keyb 0 14\r\n"
+ server.socket.data.write "\004\b\"\0179876543210\r\n"
+ server.socket.data.write "END\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ values = @cache.get_multi 'key', 'keyb'
+
+ assert_equal "get my_namespace:key my_namespace:keyb\r\n",
+ server.socket.written.string
+
+ expected = { 'key' => '0123456789', 'keyb' => '9876543210' }
+
+ assert_equal expected.sort, values.sort
+ end
+
+ def test_get_raw
+ server = FakeServer.new
+ server.socket.data.write "VALUE my_namespace:key 0 10\r\n"
+ server.socket.data.write "0123456789\r\n"
+ server.socket.data.write "END\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+
+ value = @cache.get 'key', true
+
+ assert_equal "get my_namespace:key\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal '0123456789', value
+ end
+
+ def test_get_server_for_key
+ server = @cache.get_server_for_key 'key'
+ assert_equal 'localhost', server.host
+ assert_equal 1, server.port
+ end
+
+ def test_get_server_for_key_multiple
+ s1 = util_setup_server @cache, 'one.example.com', ''
+ s2 = util_setup_server @cache, 'two.example.com', ''
+ @cache.instance_variable_set :@servers, [s1, s2]
+ @cache.instance_variable_set :@buckets, [s1, s2]
+
+ server = @cache.get_server_for_key 'keya'
+ assert_equal 'two.example.com', server.host
+ server = @cache.get_server_for_key 'keyb'
+ assert_equal 'one.example.com', server.host
+ end
+
+ def test_get_server_for_key_no_servers
+ @cache.servers = []
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.get_server_for_key 'key'
+ end
+
+ assert_equal 'No servers available', e.message
+ end
+
+ def test_get_server_for_key_spaces
+ e = assert_raise ArgumentError do
+ @cache.get_server_for_key 'space key'
+ end
+ assert_equal 'illegal character in key "space key"', e.message
+ end
+
+ def test_get_server_for_key_length
+ @cache.get_server_for_key 'x' * 250
+ long_key = 'x' * 251
+ e = assert_raise ArgumentError do
+ @cache.get_server_for_key long_key
+ end
+ assert_equal "key too long #{long_key.inspect}", e.message
+ end
+
+ def test_incr
+ server = FakeServer.new
+ server.socket.data.write "5\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.incr 'key'
+
+ assert_equal "incr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal 5, value
+ end
+
+ def test_incr_not_found
+ server = FakeServer.new
+ server.socket.data.write "NOT_FOUND\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.incr 'key'
+
+ assert_equal "incr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal nil, value
+ end
+
+ def test_incr_space_padding
+ server = FakeServer.new
+ server.socket.data.write "5 \r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ value = @cache.incr 'key'
+
+ assert_equal "incr my_namespace:key 1\r\n",
+ @cache.servers.first.socket.written.string
+
+ assert_equal 5, value
+ end
+
+ def test_make_cache_key
+ assert_equal 'my_namespace:key', @cache.make_cache_key('key')
+ @cache.namespace = nil
+ assert_equal 'key', @cache.make_cache_key('key')
+ end
+
+ def test_servers
+ server = FakeServer.new
+ @cache.servers = []
+ @cache.servers << server
+ assert_equal [server], @cache.servers
+ end
+
+ def test_servers_equals_type_error
+ e = assert_raise TypeError do
+ @cache.servers = [Object.new]
+ end
+
+ assert_equal 'cannot convert Object into MemCache::Server', e.message
+ end
+
+ def test_set
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.set 'key', 'value'
+
+ expected = "set my_namespace:key 0 0 9\r\n\004\b\"\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_set_expiry
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.set 'key', 'value', 5
+
+ expected = "set my_namespace:key 0 5 9\r\n\004\b\"\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_set_raw
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.set 'key', 'value', 0, true
+
+ expected = "set my_namespace:key 0 0 5\r\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_set_readonly
+ cache = MemCache.new :readonly => true
+
+ e = assert_raise MemCache::MemCacheError do
+ cache.set 'key', 'value'
+ end
+
+ assert_equal 'Update of readonly cache', e.message
+ end
+
+ def test_set_too_big
+ server = FakeServer.new
+ server.socket.data.write "SERVER_ERROR object too large for cache\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ e = assert_raise MemCache::MemCacheError do
+ @cache.set 'key', 'v'
+ end
+
+ assert_equal 'object too large for cache', e.message
+ end
+
+ def test_add
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.add 'key', 'value'
+
+ expected = "add my_namespace:key 0 0 9\r\n\004\b\"\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_add_exists
+ server = FakeServer.new
+ server.socket.data.write "NOT_STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.add 'key', 'value'
+
+ expected = "add my_namespace:key 0 0 9\r\n\004\b\"\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_add_expiry
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.add 'key', 'value', 5
+
+ expected = "add my_namespace:key 0 5 9\r\n\004\b\"\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_add_raw
+ server = FakeServer.new
+ server.socket.data.write "STORED\r\n"
+ server.socket.data.rewind
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.add 'key', 'value', 0, true
+
+ expected = "add my_namespace:key 0 0 5\r\nvalue\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_add_readonly
+ cache = MemCache.new :readonly => true
+
+ e = assert_raise MemCache::MemCacheError do
+ cache.add 'key', 'value'
+ end
+
+ assert_equal 'Update of readonly cache', e.message
+ end
+
+ def test_delete
+ server = FakeServer.new
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.delete 'key'
+
+ expected = "delete my_namespace:key 0\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_delete_with_expiry
+ server = FakeServer.new
+ @cache.servers = []
+ @cache.servers << server
+
+ @cache.delete 'key', 300
+
+ expected = "delete my_namespace:key 300\r\n"
+ assert_equal expected, server.socket.written.string
+ end
+
+ def test_flush_all
+ @cache.servers = []
+ 3.times { @cache.servers << FakeServer.new }
+
+ @cache.flush_all
+
+ expected = "flush_all\r\n"
+ @cache.servers.each do |server|
+ assert_equal expected, server.socket.written.string
+ end
+ end
+
+ def test_flush_all_failure
+ socket = FakeSocket.new
+ socket.data.write "ERROR\r\n"
+ socket.data.rewind
+ server = FakeServer.new socket
+ def server.host() "localhost"; end
+ def server.port() 11211; end
+
+ @cache.servers = []
+ @cache.servers << server
+
+ assert_raise MemCache::MemCacheError do
+ @cache.flush_all
+ end
+
+ assert_equal "flush_all\r\n", socket.written.string
+ end
+
+ def test_stats
+ socket = FakeSocket.new
+ socket.data.write "STAT pid 20188\r\nSTAT total_items 32\r\nSTAT version 1.2.3\r\nSTAT rusage_user 1:300\r\nSTAT dummy ok\r\nEND\r\n"
+ socket.data.rewind
+ server = FakeServer.new socket
+ def server.host() 'localhost'; end
+ def server.port() 11211; end
+
+ @cache.servers = []
+ @cache.servers << server
+
+ expected = {
+ 'localhost:11211' => {
+ 'pid' => 20188, 'total_items' => 32, 'version' => '1.2.3',
+ 'rusage_user' => 1.0003, 'dummy' => 'ok'
+ }
+ }
+ assert_equal expected, @cache.stats
+
+ assert_equal "stats\r\n", socket.written.string
+ end
+
+ def test_basic_threaded_operations_should_work
+ cache = MemCache.new :multithread => true,
+ :namespace => 'my_namespace',
+ :readonly => false
+ server = util_setup_server cache, 'example.com', "OK\r\n"
+ cache.instance_variable_set :@servers, [server]
+
+ assert_nothing_raised do
+ cache.set "test", "test value"
+ end
+ end
+
+ def test_basic_unthreaded_operations_should_work
+ cache = MemCache.new :multithread => false,
+ :namespace => 'my_namespace',
+ :readonly => false
+ server = util_setup_server cache, 'example.com', "OK\r\n"
+ cache.instance_variable_set :@servers, [server]
+
+ assert_nothing_raised do
+ cache.set "test", "test value"
+ end
+ end
+
+ def util_setup_fake_server
+ server = FakeServer.new
+ server.socket.data.write "VALUE my_namespace:key 0 14\r\n"
+ server.socket.data.write "\004\b\"\0170123456789\r\n"
+ server.socket.data.write "END\r\n"
+ server.socket.data.rewind
+
+ @cache.servers = []
+ @cache.servers << server
+
+ return server
+ end
+
+ def util_setup_server(memcache, host, responses)
+ server = MemCache::Server.new memcache, host
+ server.instance_variable_set :@sock, StringIO.new(responses)
+
+ @cache.servers = []
+ @cache.servers << server
+
+ return server
+ end
+
+end
+
Please sign in to comment.
Something went wrong with that request. Please try again.