<commit_before>module RedisFailover
  # Redis failover-aware client. RedisFailover::Client is a wrapper over a set of underlying redis
  # clients, which means all normal redis operations can be performed on an instance of this class.
  # The class only requires a set of ZooKeeper server addresses to function properly. The client
  # will automatically retry failed operations, and handle failover to a new master. The client
  # registers and listens for watcher events from the Node Manager. When these events are received,
  # the client fetches the latest set of redis nodes from ZooKeeper and rebuilds its internal
  # Redis clients appropriately. RedisFailover::Client also directs write operations to the master,
  # and all read operations to the slaves.
  #
  # @example Usage
  #   client = RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183')
  #   client.set('foo', 1) # will be directed to master
  #   client.get('foo') # will be directed to a slave
  #
  class Client
    include Util

    # Maximum allowed elapsed time between notifications from the Node Manager.
    # When this timeout is reached, the client will raise a NoNodeManagerError
    # and purge its internal redis clients.
    ZNODE_UPDATE_TIMEOUT = 9

    # 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,
      :ttl,
      :dbsize,
    ].freeze

    # Performance optimization: to avoid unnecessary method_missing calls,
    # we proactively define methods that dispatch to the underlying redis
    # calls.
    Redis.public_instance_methods(false).each do |method|
      define_method(method) do |*args, &block|
        dispatch(method, *args, &block)
      end
    end

    # Creates a new failover redis client.
    #
    # @param [Hash] options the options used to initialize the client instance
    # @option options [String] :zkservers comma-separated ZooKeeper host:port pairs (required)
    # @option options [String] :znode_path znode path override for redis server list
    # @option options [String] :password password for redis nodes
    # @option options [String] :db database to use for redis nodes
    # @option options [String] :namespace namespace for redis nodes
    # @option options [Logger] :logger logger override
    # @option options [Boolean] :retry_failure indicates if failures should be retried
    # @option options [Integer] :max_retries max retries for a failure
    # @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 = []
      @queue = Queue.new
      @lock = Monitor.new
      setup_zk
      build_clients
    end

    # Dispatches redis operations to master/slaves.
    def method_missing(method, *args, &block)
      if redis_operation?(method)
        dispatch(method, *args, &block)
      else
        super
      end
    end

    # Determines whether or not an unknown method can be handled.
    #
    # @param [Symbol] method the method to check
    # @param [Boolean] include_private determines if private methods should be checked
    # @return [Boolean] indicates if the method can be handled
    def respond_to_missing?(method, include_private)
      redis_operation?(method) || super
    end

    # @return [String] a string representation of the client
    def inspect
      "#<RedisFailover::Client (master: #{master_name}, slaves: #{slave_names})>"
    end
    alias_method :to_s, :inspect

    # Force a manual failover to a new server. A specific server can be specified
    # via options. If no options are passed, a random slave will be selected as
    # the candidate for the new master.
    #
    # @param [Hash] options the options used for manual failover
    # @option options [String] :host the host of the failover candidate
    # @option options [String] :port the port of the failover candidate
    def manual_failover(options = {})
      Manual.failover(zk, options)
      self
    end

    # Gracefully performs a shutdown of this client. This method is
    # mostly useful when the client is used in a forking environment.
    # When a fork occurs, you can call this method in an after_fork hook,
    # and then create a new instance of the client. The underlying
    # ZooKeeper client and redis clients will be closed.
    def shutdown
      @zk.close! if @zk
      purge_clients
    end

    private

    # Sets up the underlying ZooKeeper connection.
    def setup_zk
      @zk = ZK.new(@zkservers)
      @zk.watcher.register(@znode) { |event| handle_zk_event(event) }
      @zk.on_expired_session { purge_clients }
      @zk.on_connected { @zk.stat(@znode, :watch => true) }
      @zk.stat(@znode, :watch => true)
      update_znode_timestamp
    end

    # Handles a ZK event.
    #
    # @param [ZK::Event] event the ZK event to handle
    def handle_zk_event(event)
      update_znode_timestamp
      if event.node_created? || event.node_changed?
        build_clients
      elsif event.node_deleted?<commit_msg>provide a friendly reopen method for after-forking<commit_after>module RedisFailover
  # Redis failover-aware client. RedisFailover::Client is a wrapper over a set of underlying redis
  # clients, which means all normal redis operations can be performed on an instance of this class.
  # The class only requires a set of ZooKeeper server addresses to function properly. The client
  # will automatically retry failed operations, and handle failover to a new master. The client
  # registers and listens for watcher events from the Node Manager. When these events are received,
  # the client fetches the latest set of redis nodes from ZooKeeper and rebuilds its internal
  # Redis clients appropriately. RedisFailover::Client also directs write operations to the master,
  # and all read operations to the slaves.
  #
  # @example Usage
  #   client = RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183')
  #   client.set('foo', 1) # will be directed to master
  #   client.get('foo') # will be directed to a slave
  #
  class Client
    include Util

    # Maximum allowed elapsed time between notifications from the Node Manager.
    # When this timeout is reached, the client will raise a NoNodeManagerError
    # and purge its internal redis clients.
    ZNODE_UPDATE_TIMEOUT = 9

    # 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,
      :ttl,
      :dbsize,
    ].freeze

    # Performance optimization: to avoid unnecessary method_missing calls,
    # we proactively define methods that dispatch to the underlying redis
    # calls.
    Redis.public_instance_methods(false).each do |method|
      define_method(method) do |*args, &block|
        dispatch(method, *args, &block)
      end
    end

    # Creates a new failover redis client.
    #
    # @param [Hash] options the options used to initialize the client instance
    # @option options [String] :zkservers comma-separated ZooKeeper host:port pairs (required)
    # @option options [String] :znode_path znode path override for redis server list
    # @option options [String] :password password for redis nodes
    # @option options [String] :db database to use for redis nodes
    # @option options [String] :namespace namespace for redis nodes
    # @option options [Logger] :logger logger override
    # @option options [Boolean] :retry_failure indicates if failures should be retried
    # @option options [Integer] :max_retries max retries for a failure
    # @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 = []
      @queue = Queue.new
      @lock = Monitor.new
      setup_zk
      build_clients
    end

    # Dispatches redis operations to master/slaves.
    def method_missing(method, *args, &block)
      if redis_operation?(method)
        dispatch(method, *args, &block)
      else
        super
      end
    end

    # Determines whether or not an unknown method can be handled.
    #
    # @param [Symbol] method the method to check
    # @param [Boolean] include_private determines if private methods should be checked
    # @return [Boolean] indicates if the method can be handled
    def respond_to_missing?(method, include_private)
      redis_operation?(method) || super
    end

    # @return [String] a string representation of the client
    def inspect
      "#<RedisFailover::Client (master: #{master_name}, slaves: #{slave_names})>"
    end
    alias_method :to_s, :inspect

    # Force a manual failover to a new server. A specific server can be specified
    # via options. If no options are passed, a random slave will be selected as
    # the candidate for the new master.
    #
    # @param [Hash] options the options used for manual failover
    # @option options [String] :host the host of the failover candidate
    # @option options [String] :port the port of the failover candidate
    def manual_failover(options = {})
      Manual.failover(zk, options)
      self
    end

    # Gracefully performs a shutdown of this client. This method is
    # mostly useful when the client is used in a forking environment.
    # When a fork occurs, you can call this method in an after_fork hook,
    # and then create a new instance of the client. The underlying
    # ZooKeeper client and redis clients will be closed.
    def shutdown
      @zk.close! if @zk
      @zk = nil
      purge_clients
    end

    # Reopen will first perform a shutdown of the underlying ZooKeeper client
    # and redis clients. Next, it attempts to rebuild the ZooKeeper client
    # and re-create the redis clients after it fetches the most up-to-date
    # list from ZooKeeper.
    def reopen
      shutdown
      setup_zk
      build_clients
    end

    private

    # Sets up the underlying ZooKeeper connection.
    def setup_zk
      @zk = ZK.new(@zkservers)
      @zk.watcher.register(@znode) { |event| handle_zk_event(event) }
      @zk.on_expired_session { purge_clients }
      @zk.on_connected { @zk.stat(@znode, :watch => true) }
      @zk.stat(@znode, :watch => true)
      update_znode_timestamp
    end

    # Handles a ZK event.
    #
    # @param [ZK::Event] event the ZK event to handle
    def handle_zk_event(event)
      update_znode_timestamp
      if event.node_created? || event.node_changed?
        build_clients
      elsif event.node_deleted?
{'fraction_non_alphanumeric': 0.07882641335988969, 'fraction_numerical': 0.002604565650375364, 'mean_word_length': 2.8612836438923397, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '491437', 'n_tokens_mistral': 3856, 'n_tokens_neox': 3770, 'n_words': 1381}