diff options
Diffstat (limited to 'activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb')
-rw-r--r-- | activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb | 935 |
1 files changed, 935 insertions, 0 deletions
diff --git a/activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb b/activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb new file mode 100644 index 0000000000..4d594c2089 --- /dev/null +++ b/activesupport/lib/active_support/vendor/memcache-client-1.6.5/memcache.rb @@ -0,0 +1,935 @@ +$TESTING = defined?($TESTING) && $TESTING + +require 'socket' +require 'thread' +require 'timeout' +require 'zlib' +require 'digest/sha1' + +## +# A Ruby client library for memcached. +# + +class MemCache + + ## + # The version of MemCache you are using. + + VERSION = '1.6.4.99' + + ## + # Default options for the cache object. + + DEFAULT_OPTIONS = { + :namespace => nil, + :readonly => false, + :multithread => true, + :failover => true, + :timeout => 0.5, + :logger => nil, + } + + ## + # Default memcached port. + + DEFAULT_PORT = 11211 + + ## + # Default memcached server weight. + + DEFAULT_WEIGHT = 1 + + ## + # 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 + + ## + # Socket timeout limit with this client, defaults to 0.5 sec. + # Set to nil to disable timeouts. + + attr_reader :timeout + + ## + # Should the client try to failover to another server if the + # first server is down? Defaults to true. + + attr_reader :failover + + ## + # Log debug/info/warn/error to the given Logger, defaults to nil. + + attr_reader :logger + + ## + # 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 exception on cache writes when true. + # [:multithread] Wraps cache access in a Mutex for thread safety. + # [:failover] Should the client try to failover to another server if the + # first server is down? Defaults to true. + # [:timeout] Time to use as the socket read timeout. Defaults to 0.5 sec, + # set to nil to disable timeouts (this is a major performance penalty in Ruby 1.8). + # [:logger] Logger to use for info/debug output, defaults to nil + # 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] + @timeout = opts[:timeout] + @failover = opts[:failover] + @logger = opts[:logger] + @mutex = Mutex.new if @multithread + + logger.info { "memcache-client #{VERSION} #{Array(servers).inspect}" } if logger + + Thread.current[:memcache_client] = self.object_id if !@multithread + + self.servers = servers + end + + ## + # Returns a string representation of the cache object. + + def inspect + "<MemCache: %d servers, ns: %p, ro: %p>" % + [@servers.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 = Array(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 + else + server + end + end + + logger.debug { "Servers now: #{@servers.inspect}" } if logger + + # There's no point in doing this if there's only one server + @continuum = create_continuum_for(@servers) if @servers.size > 1 + + @servers + end + + ## + # Decrements 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) + raise MemCacheError, "Update of readonly cache" if @readonly + with_server(key) do |server, cache_key| + cache_decr server, cache_key, amount + end + rescue TypeError => err + handle_error nil, err + end + + ## + # Retrieves +key+ from memcache. If +raw+ is false, the value will be + # unmarshalled. + + def get(key, raw = false) + with_server(key) do |server, cache_key| + value = cache_get server, cache_key + logger.debug { "GET #{key} from #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger + return nil if value.nil? + value = Marshal.load value unless raw + return value + end + rescue TypeError => err + handle_error nil, 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 } + # + # Note that get_multi assumes the values are marshalled. + + 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_for_server| + keys_for_server_str = keys_for_server.join ' ' + begin + values = cache_get_multi server, keys_for_server_str + values.each do |key, value| + results[cache_keys[key]] = Marshal.load value + end + rescue IndexError => e + # Ignore this server and try the others + logger.warn { "Unable to retrieve #{keys_for_server.size} elements from #{server.inspect}: #{e.message}"} if logger + end + end + + return results + rescue TypeError => err + handle_error nil, err + end + + ## + # Increments 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. + + def incr(key, amount = 1) + raise MemCacheError, "Update of readonly cache" if @readonly + with_server(key) do |server, cache_key| + cache_incr server, cache_key, amount + end + rescue TypeError => err + handle_error nil, 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. + + ONE_MB = 1024 * 1024 + + def set(key, value, expiry = 0, raw = false) + raise MemCacheError, "Update of readonly cache" if @readonly + with_server(key) do |server, cache_key| + + value = Marshal.dump value unless raw + logger.debug { "SET #{key} to #{server.inspect}: #{value ? value.to_s.size : 'nil'}" } if logger + + data = value.to_s + raise MemCacheError, "Value too large, memcached can only store 1MB of data per key" if data.size > ONE_MB + + command = "set #{cache_key} 0 #{expiry} #{data.size}\r\n#{data}\r\n" + + with_socket_management(server) do |socket| + socket.write command + result = socket.gets + raise_on_error_response! result + + if result.nil? + server.close + raise MemCacheError, "lost connection to #{server.host}:#{server.port}" + end + + result + end + 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 + with_server(key) do |server, cache_key| + value = Marshal.dump value unless raw + logger.debug { "ADD #{key} to #{server}: #{value ? value.to_s.size : 'nil'}" } if logger + command = "add #{cache_key} 0 #{expiry} #{value.to_s.size}\r\n#{value}\r\n" + + with_socket_management(server) do |socket| + socket.write command + result = socket.gets + raise_on_error_response! result + result + end + end + end + + ## + # Removes +key+ from the cache in +expiry+ seconds. + + def delete(key, expiry = 0) + raise MemCacheError, "Update of readonly cache" if @readonly + with_server(key) do |server, cache_key| + with_socket_management(server) do |socket| + socket.write "delete #{cache_key} #{expiry}\r\n" + result = socket.gets + raise_on_error_response! result + result + end + end + 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 + @servers.each do |server| + with_socket_management(server) do |socket| + socket.write "flush_all\r\n" + result = socket.gets + raise_on_error_response! result + result + end + end + rescue IndexError => err + handle_error nil, err + 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| + next unless server.alive? + + with_socket_management(server) do |socket| + value = nil + socket.write "stats\r\n" + stats = {} + while line = socket.gets do + raise_on_error_response! line + break if line == "END\r\n" + if line =~ /\ASTAT ([\S]+) ([\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 =~ /\A\d+\Z/ then + value.to_i + else + value + end + end + end + end + server_stats["#{server.host}:#{server.port}"] = stats + end + end + + raise MemCacheError, "No active servers" if server_stats.empty? + 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 + + ## + # Returns an interoperable hash value for +key+. (I think, docs are + # sketchy for down servers). + + def hash_for(key) + Zlib.crc32(key) + end + + ## + # Pick a server to handle the request based on a hash of the key. + + def get_server_for_key(key, options = {}) + 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| + entryidx = Continuum.binary_search(@continuum, hkey) + server = @continuum[entryidx].server + return server if server.alive? + break unless failover + hkey = hash_for "#{try}#{key}" + end + + raise MemCacheError, "No servers available" + end + + ## + # Performs a raw decr for +cache_key+ from +server+. Returns nil if not + # found. + + def cache_decr(server, cache_key, amount) + with_socket_management(server) do |socket| + socket.write "decr #{cache_key} #{amount}\r\n" + text = socket.gets + raise_on_error_response! text + return nil if text == "NOT_FOUND\r\n" + return text.to_i + end + end + + ## + # Fetches the raw data for +cache_key+ from +server+. Returns nil on cache + # miss. + + def cache_get(server, cache_key) + with_socket_management(server) do |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 + + raise_on_error_response! keyline + 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 + end + + ## + # Fetches +cache_keys+ from +server+ using a multi-get. + + def cache_get_multi(server, cache_keys) + with_socket_management(server) do |socket| + values = {} + socket.write "get #{cache_keys}\r\n" + + while keyline = socket.gets do + return values if keyline == "END\r\n" + raise_on_error_response! keyline + + unless keyline =~ /\AVALUE (.+) (.+) (.+)/ 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}" # TODO: retry here too + end + end + + ## + # Performs a raw incr for +cache_key+ from +server+. Returns nil if not + # found. + + def cache_incr(server, cache_key, amount) + with_socket_management(server) do |socket| + socket.write "incr #{cache_key} #{amount}\r\n" + text = socket.gets + raise_on_error_response! text + return nil if text == "NOT_FOUND\r\n" + return text.to_i + end + end + + ## + # Gets or creates a socket connected to the given server, and yields it + # to the block, wrapped in a mutex synchronization if @multithread is true. + # + # If a socket error (SocketError, SystemCallError, IOError) or protocol error + # (MemCacheError) is raised by the block, closes the socket, attempts to + # connect again, and retries the block (once). If an error is again raised, + # reraises it as MemCacheError. + # + # If unable to connect to the server (or if in the reconnect wait period), + # raises MemCacheError. Note that the socket connect code marks a server + # dead for a timeout period, so retrying does not apply to connection attempt + # failures (but does still apply to unexpectedly lost connections etc.). + + def with_socket_management(server, &block) + check_multithread_status! + + @mutex.lock if @multithread + retried = false + + begin + socket = server.socket + + # Raise an IndexError to show this server is out of whack. If were inside + # a with_server block, we'll catch it and attempt to restart the operation. + + raise IndexError, "No connection to server (#{server.status})" if socket.nil? + + block.call(socket) + + rescue SocketError => err + logger.warn { "Socket failure: #{err.message}" } if logger + server.mark_dead(err) + handle_error(server, err) + + rescue MemCacheError, SystemCallError, IOError => err + logger.warn { "Generic failure: #{err.class.name}: #{err.message}" } if logger + handle_error(server, err) if retried || socket.nil? + retried = true + retry + end + ensure + @mutex.unlock if @multithread + end + + def with_server(key) + retried = false + begin + server, cache_key = request_setup(key) + yield server, cache_key + rescue IndexError => e + logger.warn { "Server failed: #{e.class.name}: #{e.message}" } if logger + if !retried && @servers.size > 1 + logger.info { "Connection to server #{server.inspect} DIED! Retrying operation..." } if logger + retried = true + retry + end + handle_error(nil, e) + end + end + + ## + # Handles +error+ from +server+. + + def handle_error(server, error) + raise error if error.is_a?(MemCacheError) + 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 + return server, cache_key + end + + def raise_on_error_response!(response) + if response =~ /\A(?:CLIENT_|SERVER_)?ERROR(.*)/ + raise MemCacheError, $1.strip + end + end + + def create_continuum_for(servers) + total_weight = servers.inject(0) { |memo, srv| memo + srv.weight } + continuum = [] + + servers.each do |server| + entry_count_for(server, servers.size, total_weight).times do |idx| + hash = Digest::SHA1.hexdigest("#{server.host}:#{server.port}:#{idx}") + value = Integer("0x#{hash[0..7]}") + continuum << Continuum::Entry.new(value, server) + end + end + + continuum.sort { |a, b| a.value <=> b.value } + end + + def entry_count_for(server, total_servers, total_weight) + ((total_servers * Continuum::POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor + end + + def check_multithread_status! + return if @multithread + + if Thread.current[:memcache_client] != self.object_id + raise MemCacheError, <<-EOM + You are accessing this memcache-client instance from multiple threads but have not enabled multithread support. + Normally: MemCache.new(['localhost:11211'], :multithread => true) + In Rails: config.cache_store = [:mem_cache_store, 'localhost:11211', { :multithread => true }] + EOM + end + 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 + + attr_reader :logger + + ## + # 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? + + @host = host + @port = port.to_i + @weight = weight.to_i + + @sock = nil + @retry = nil + @status = 'NOT CONNECTED' + @timeout = memcache.timeout + @logger = memcache.logger + 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 + 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 ? TCPTimeoutSocket.new(@host, @port, @timeout) : TCPSocket.new(@host, @port) + + 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 + logger.warn { "Unable to open socket: #{err.class.name}, #{err.message}" } if logger + mark_dead err + end + + return @sock + end + + ## + # Close the connection to the memcached server targeted by this + # object. The server is not considered dead. + + def close + @sock.close if @sock && !@sock.closed? + @sock = nil + @retry = nil + @status = "NOT CONNECTED" + end + + ## + # Mark the server as dead and close its socket. + + def mark_dead(error) + @sock.close if @sock && !@sock.closed? + @sock = nil + @retry = Time.now + RETRY_DELAY + + reason = "#{error.class.name}: #{error.message}" + @status = sprintf "%s:%s DEAD (%s), will retry at %s", @host, @port, reason, @retry + @logger.info { @status } if @logger + end + + end + + ## + # Base MemCache exception class. + + class MemCacheError < RuntimeError; end + +end + +# TCPSocket facade class which implements timeouts. +class TCPTimeoutSocket + + def initialize(host, port, timeout) + Timeout::timeout(MemCache::Server::CONNECT_TIMEOUT, SocketError) do + @sock = TCPSocket.new(host, port) + @len = timeout + end + end + + def write(*args) + Timeout::timeout(@len, SocketError) do + @sock.write(*args) + end + end + + def gets(*args) + Timeout::timeout(@len, SocketError) do + @sock.gets(*args) + end + end + + def read(*args) + Timeout::timeout(@len, SocketError) do + @sock.read(*args) + end + end + + def _socket + @sock + end + + def method_missing(meth, *args) + @sock.__send__(meth, *args) + end + + def closed? + @sock.closed? + end + + def close + @sock.close + end +end + +module Continuum + POINTS_PER_SERVER = 160 # this is the default in libmemcached + + # Find the closest index in Continuum with value <= the given value + def self.binary_search(ary, value, &block) + upper = ary.size - 1 + lower = 0 + idx = 0 + + while(lower <= upper) do + idx = (lower + upper) / 2 + comp = ary[idx].value <=> value + + if comp == 0 + return idx + elsif comp > 0 + upper = idx - 1 + else + lower = idx + 1 + end + end + return upper + end + + class Entry + attr_reader :value + attr_reader :server + + def initialize(val, srv) + @value = val + @server = srv + end + + def inspect + "<#{value}, #{server.host}:#{server.port}>" + end + end +end |