Permalink
Browse files

new redis_failover features: master_only option, safe_mode option, on…

…_node_change callback
  • Loading branch information...
1 parent c2396bf commit 8ef4e12e1e1a1a58e89f4baaf689494c07af1ffa @ryanlecompte ryanlecompte committed Aug 13, 2012
Showing with 109 additions and 58 deletions.
  1. +4 −0 Changes.md
  2. +10 −0 README.md
  3. +47 −56 lib/redis_failover/client.rb
  4. +1 −1 lib/redis_failover/node_manager.rb
  5. +47 −1 lib/redis_failover/util.rb
View
@@ -1,6 +1,10 @@
HEAD
-----------
- Make Node Manager's lock path vary with its main znode. (Bira)
+- Node Manager's znode for holding current list of redis nodes is no longer ephemeral. This is unnecessary since the current master should only be changed by redis_failover.
+- Introduce :master_only option for RedisFailover::Client (disabled by default). This option configures the client to direct all read/write operations to the master.
+- Introduce :safe_mode option (enabled by default). This option configures the client to purge its redis clients when a ZK session expires or when the client hasn't recently heard from the node manager.
+- Introduce RedisFailover::Client#on_node_change callback notification for when the currently known list of master/slave redis nodes changes.
0.8.9
-----------
View
@@ -129,6 +129,16 @@ The full set of options that can be passed to RedisFailover::Client are:
:logger - logger override (optional)
:retry_failure - indicate if failures should be retried (default true)
:max_retries - max retries for a failure (default 3)
+ :safe_mode - indicates if safe mode is used or not (default true)
+ :master_only - indicates if only redis master is used (default false)
+
+The RedisFailover::Client also supports a custom callback that will be invoked whenever the list of redis clients changes. Example usage:
+
+ RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183') do |client|
+ client.on_node_change do |master, slaves|
+ logger.info("Nodes changed! master: #{master}, slaves: #{slaves}")
+ end
+ end
## Manual Failover
@@ -27,52 +27,6 @@ class Client
# Amount of time to sleep before retrying a failed operation.
RETRY_WAIT_TIME = 3
- # Redis read operations that are automatically dispatched to slaves. Any
- # operation not listed here will be dispatched to the master.
- REDIS_READ_OPS = Set[
- :echo,
- :exists,
- :get,
- :getbit,
- :getrange,
- :hexists,
- :hget,
- :hgetall,
- :hkeys,
- :hlen,
- :hmget,
- :hvals,
- :keys,
- :lindex,
- :llen,
- :lrange,
- :mapped_hmget,
- :mapped_mget,
- :mget,
- :scard,
- :sdiff,
- :sinter,
- :sismember,
- :smembers,
- :srandmember,
- :strlen,
- :sunion,
- :type,
- :zcard,
- :zcount,
- :zrange,
- :zrangebyscore,
- :zrank,
- :zrevrange,
- :zrevrangebyscore,
- :zrevrank,
- :zscore
- ].freeze
-
- # Unsupported Redis operations. These don't make sense in a client
- # that abstracts the master/slave servers.
- UNSUPPORTED_OPS = Set[:select, :dbsize].freeze
-
# Performance optimization: to avoid unnecessary method_missing calls,
# we proactively define methods that dispatch to the underlying redis
# calls.
@@ -93,25 +47,37 @@ class Client
# @option options [Logger] :logger logger override
# @option options [Boolean] :retry_failure indicates if failures are retried
# @option options [Integer] :max_retries max retries for a failure
+ # @option options [Boolean] :safe_mode indicates if safe mode is used or not
+ # @option options [Boolean] :master_only indicates if only redis master is used
# @return [RedisFailover::Client]
def initialize(options = {})
Util.logger = options[:logger] if options[:logger]
- @zkservers = options.fetch(:zkservers) { raise ArgumentError, ':zkservers required'}
- @znode = options[:znode_path] || Util::DEFAULT_ZNODE_PATH
- @namespace = options[:namespace]
- @password = options[:password]
- @db = options[:db]
- @retry = options[:retry_failure] || true
- @max_retries = @retry ? options.fetch(:max_retries, 3) : 0
@master = nil
@slaves = []
@node_addresses = {}
@lock = Monitor.new
@current_client_key = "current-client-#{self.object_id}"
+ yield self if block_given?
+
+ parse_options(options)
setup_zk
build_clients
end
+ # Specifies a callback to invoke when the current redis node list changes.
+ #
+ # @param [Proc] a callback with current master and slaves as arguments
+ #
+ # @example Usage
+ # RedisFailover::Client.new(:zkservers => zk_servers) do |client|
+ # client.on_node_change do |master, slaves|
+ # logger.info("Nodes changed! master: #{master}, slaves: #{slaves}")
+ # end
+ # end
+ def on_node_change(&callback)
+ @on_node_change = callback
+ end
+
# Dispatches redis operations to master/slaves.
def method_missing(method, *args, &block)
if redis_operation?(method)
@@ -174,7 +140,7 @@ def reconnect
def setup_zk
@zk = ZK.new(@zkservers)
@zk.watcher.register(@znode) { |event| handle_zk_event(event) }
- @zk.on_expired_session { purge_clients }
+ @zk.on_expired_session { purge_clients if @safe_mode }
@zk.on_connected { @zk.stat(@znode, :watch => true) }
@zk.stat(@znode, :watch => true)
update_znode_timestamp
@@ -210,7 +176,7 @@ def redis_operation?(method)
# @param [Proc] block an optional block to pass to the method
# @return [Object] the result of dispatching the command
def dispatch(method, *args, &block)
- unless recently_heard_from_node_manager?
+ if @safe_mode && !recently_heard_from_node_manager?
build_clients
end
@@ -274,6 +240,7 @@ def build_clients
new_slaves = new_clients_for(*nodes[:slaves])
@master = new_master
@slaves = new_slaves
+ @on_node_change.call(master_name, slave_names) if @on_node_change
rescue
purge_clients
raise
@@ -424,7 +391,16 @@ def recently_heard_from_node_manager?
# nested blocks (e.g., block passed to multi).
def client_for(method)
stack = Thread.current[@current_client_key] ||= []
- client = stack.last || (REDIS_READ_OPS.include?(method) ? slave : master)
+ client = if stack.last
+ stack.last
+ elsif @master_only
+ master
+ elsif REDIS_READ_OPS.include?(method)
+ slave
+ else
+ master
+ end
+
stack << client
client
end
@@ -436,5 +412,20 @@ def free_client
end
nil
end
+
+ # Parses the configuration operations.
+ #
+ # @param [Hash] options the configuration options
+ def parse_options(options)
+ @zkservers = options.fetch(:zkservers) { raise ArgumentError, ':zkservers required'}
+ @znode = options.fetch(:znode_path, Util::DEFAULT_ZNODE_PATH)
+ @namespace = options[:namespace]
+ @password = options[:password]
+ @db = options[:db]
+ @retry = options.fetch(:retry_failure, true)
+ @max_retries = @retry ? options.fetch(:max_retries, 3) : 0
+ @safe_mode = options.fetch(:safe_mode, true)
+ @master_only = options.fetch(:master_only, false)
+ end
end
end
@@ -315,7 +315,7 @@ def delete_path
# Creates the znode path containing the redis nodes.
def create_path
unless @zk.exists?(@znode)
- @zk.create(@znode, encode(current_nodes), :ephemeral => true)
+ @zk.create(@znode, encode(current_nodes))
logger.info("Created ZooKeeper node #{@znode}")
end
rescue ZK::Exceptions::NodeExists
View
@@ -5,14 +5,60 @@ module RedisFailover
module Util
extend self
+ # Redis read operations that are automatically dispatched to slaves. Any
+ # operation not listed here will be dispatched to the master.
+ REDIS_READ_OPS = Set[
+ :echo,
+ :exists,
+ :get,
+ :getbit,
+ :getrange,
+ :hexists,
+ :hget,
+ :hgetall,
+ :hkeys,
+ :hlen,
+ :hmget,
+ :hvals,
+ :keys,
+ :lindex,
+ :llen,
+ :lrange,
+ :mapped_hmget,
+ :mapped_mget,
+ :mget,
+ :scard,
+ :sdiff,
+ :sinter,
+ :sismember,
+ :smembers,
+ :srandmember,
+ :strlen,
+ :sunion,
+ :type,
+ :zcard,
+ :zcount,
+ :zrange,
+ :zrangebyscore,
+ :zrank,
+ :zrevrange,
+ :zrevrangebyscore,
+ :zrevrank,
+ :zscore
+ ].freeze
+
+ # Unsupported Redis operations. These don't make sense in a client
+ # that abstracts the master/slave servers.
+ UNSUPPORTED_OPS = Set[:select, :dbsize].freeze
+
# Default node in ZK that contains the current list of available redis nodes.
DEFAULT_ZNODE_PATH = '/redis_failover_nodes'.freeze
# Connectivity errors that the redis (<3.x) client raises.
REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }
# Connectivity errors that the redis (>3.x) client raises.
- REDIS_ERRORS << Redis::BaseError if Redis.const_defined?("BaseError")
+ REDIS_ERRORS << Redis::BaseError if Redis.const_defined?('BaseError')
REDIS_ERRORS.freeze
# Full set of errors related to connectivity.

0 comments on commit 8ef4e12

Please sign in to comment.