# coding: utf-8 require 'redis' require 'redis/namespace' require 'stockpile/base' class Stockpile # A connection manager for Redis. class Redis < Stockpile::Base VERSION = '1.1' # :nodoc: # Create a new Redis connection manager with the provided options. # # == Options # # +redis+:: Provides the Redis connection options. # +namespace+:: Provides the Redis namespace to use, but only if # redis-namespace is in use (detected with the existence of # Redis::Namespace). # # The namespace can also be provided as a key in the +redis+ # options if it is missing from the main options. If there is # no namespace key present in either +options+ or # options[:redis], a namespace will be generated # from one of the following: $REDIS_NAMESPACE, # Rails.env (if in Rails), or $RACK_ENV. def initialize(options = {}) super @redis_options = (@options.delete(:redis) || {}).dup @namespace = @options.fetch(:namespace) { @redis_options.fetch(:namespace) { ENV['REDIS_NAMESPACE'] || (defined?(::Rails) && ::Rails.env) || ENV['RACK_ENV'] } } @options.delete(:namespace) @redis_options.delete(:namespace) end ## # Connect to Redis, unless already connected. Additional client connections # can be specified in the parameters as a shorthand for calls to # #connection_for. # # If #narrow? is true, the same Redis connection will be used for all # clients managed by this connection manager. # # manager.connect # manager.connection_for(:redis) # # # This means the same as above. # manager.connect(:redis) # # === Clients # # +clients+ may be provided in one of several ways: # # * A Hash object, mapping client names to client options. # # connect(redis: nil, rollout: { namespace: 'rollout' }) # # Transforms into: # # connect(redis: {}, rollout: { namespace: 'rollout' }) # # * An (implicit) array of client names, for connections with no options # provided. # # connect(:redis, :resque, :rollout) # # Transforms into: # # connect(redis: {}, resque: {}, rollout: {}) # # * An array of Hash objects, mapping client names to client options. # # connect({ redis: nil }, # { rollout: { namespace: 'rollout' } }) # # Transforms into: # # connect(redis: {}, rollout: { namespace: 'rollout' }) # # * A mix of client names and Hash objects: # # connect(:redis, { rollout: { namespace: 'rollout' } }) # # Transforms into: # # connect(redis: {}, rollout: { namespace: 'rollout' }) # # ==== Client Options # # Stockpile::Redis supports one option, +namespace+. The use of this with a # client automatically wraps the client in the provided +namespace+. The # namespace will be within the global namespace for the Stockpile::Redis # instance, if one has been set. # # r = Stockpile::Redis.new(namespace: 'first') # rr = r.client_for(other: { namespace: 'second' }) # rr.set 'found', true # r.set 'found', true # rr.keys # => [ 'found' ] # r.keys # => [ 'found', 'second:found' ] # r.connection.redis.keys # => [ 'first:found', 'first:second:found' ] # # :method: connect ## # Returns a Redis client connection for a particular client. If the # connection manager is using a narrow connection width, this returns the # same as #connection. # # The +client_name+ of +:all+ will always return +nil+. # # If the requested client does not yet exist, the connection will be # created with the provided options. # # Because Resque depends on Redis::Namespace, #connection_for will perform # special Redis::Namespace handling for a connection with the name # +:resque+. # # :method: connection_for ## # Reconnect to Redis for some or all clients. The primary connection will # always be reconnected; other clients will be reconnected based on the # +clients+ provided. Only clients actively managed by previous calls to # #connect or #connection_for will be reconnected. # # If #reconnect is called with the value +:all+, all currently managed # clients will be reconnected. If #narrow? is true, the primary connection # will be reconnected. # # :method: reconnect ## # Disconnect from Redis for some or all clients. The primary connection # will always be disconnected; other clients will be disconnected based on # the +clients+ provided. Only clients actively managed by previous calls # to #connect or #connection_for will be disconnected. # # If #disconnect is called with the value +:all+, all currently managed # clients will be disconnected. If #narrow? is true, the primary connection # will be disconnected. # # :method: disconnect private def client_connect(name = nil, options = {}) options = { namespace: options[:namespace] } case name when :resque connect_for_resque(options) else connect_for_any(options) end end def client_reconnect(redis = connection()) redis.client.reconnect if redis end def client_disconnect(redis = connection()) redis.quit if redis end def connect_for_any(options) r = if connection && narrow? connection else r = ::Redis.new(@redis_options.merge(options)) if @namespace ::Redis::Namespace.new(@namespace, redis: r) else r end end if options[:namespace] r = ::Redis::Namespace.new(options[:namespace], redis: r) end r end def connect_for_resque(options) r = connect_for_any(options) if r.instance_of?(::Redis::Namespace) && r.namespace.to_s !~ /:resque\z/ r = ::Redis::Namespace.new(:"#{r.namespace}:resque", redis: r.redis) elsif r.instance_of?(::Redis) r = ::Redis::Namespace.new("resque", redis: r) end r end end # Enables module or class +mod+ to contain a Stockpile instance that defaults # the created +cache+ method to using Stockpile::Redis as the key-value store # connection manager. # # See Stockpile.inject!. def self.inject_redis!(mod, options = {}) inject!(mod, options.merge(default_manager: Stockpile::Redis)) end end