@@ -31,12 +31,16 @@ def self.current=(redis)
3131 # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
3232 # @option options [Array] :sentinels List of sentinels to contact
3333 # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
34+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
35+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
3436 # @option options [Class] :connector Class of custom connector
3537 #
3638 # @return [Redis] a new client instance
3739 def initialize ( options = { } )
3840 @options = options . dup
39- @original_client = @client = Client . new ( options )
41+ @cluster_mode = options . key? ( :cluster )
42+ client = @cluster_mode ? Cluster : Client
43+ @original_client = @client = client . new ( options )
4044 @queue = Hash . new { |h , k | h [ k ] = [ ] }
4145
4246 super ( ) # Monitor#initialize
@@ -274,9 +278,7 @@ def info(cmd = nil)
274278 synchronize do |client |
275279 client . call ( [ :info , cmd ] . compact ) do |reply |
276280 if reply . kind_of? ( String )
277- reply = Hash [ reply . split ( "\r \n " ) . map do |line |
278- line . split ( ":" , 2 ) unless line =~ /^(#|$)/
279- end . compact ]
281+ reply = HashifyInfo . call ( reply )
280282
281283 if cmd && cmd . to_s == "commandstats"
282284 # Extract nested hashes for INFO COMMANDSTATS
@@ -2818,6 +2820,41 @@ def sentinel(subcommand, *args)
28182820 end
28192821 end
28202822
2823+ # Sends `CLUSTER *` command to random node and returns its reply.
2824+ #
2825+ # @see https://redis.io/commands#cluster Reference of cluster command
2826+ #
2827+ # @param subcommand [String, Symbol] the subcommand of cluster command
2828+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
2829+ #
2830+ # @return [Object] depends on the subcommand
2831+ def cluster ( subcommand , *args )
2832+ subcommand = subcommand . to_s . downcase
2833+ block = case subcommand
2834+ when 'slots' then HashifyClusterSlots
2835+ when 'nodes' then HashifyClusterNodes
2836+ when 'slaves' then HashifyClusterSlaves
2837+ when 'info' then HashifyInfo
2838+ else Noop
2839+ end
2840+
2841+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
2842+ block = Noop unless @cluster_mode
2843+
2844+ synchronize do |client |
2845+ client . call ( [ :cluster , subcommand ] + args , &block )
2846+ end
2847+ end
2848+
2849+ # Sends `ASKING` command to random node and returns its reply.
2850+ #
2851+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
2852+ #
2853+ # @return [String] `'OK'`
2854+ def asking
2855+ synchronize { |client | client . call ( %i[ asking ] ) }
2856+ end
2857+
28212858 def id
28222859 @original_client . id
28232860 end
@@ -2831,6 +2868,8 @@ def dup
28312868 end
28322869
28332870 def connection
2871+ return @original_client . connection_info if @cluster_mode
2872+
28342873 {
28352874 host : @original_client . host ,
28362875 port : @original_client . port ,
@@ -2896,6 +2935,56 @@ def method_missing(command, *args)
28962935 end
28972936 }
28982937
2938+ HashifyInfo =
2939+ lambda { |reply |
2940+ Hash [ reply . split ( "\r \n " ) . map do |line |
2941+ line . split ( ':' , 2 ) unless line =~ /^(#|$)/
2942+ end . compact ]
2943+ }
2944+
2945+ HashifyClusterNodeInfo =
2946+ lambda { |str |
2947+ arr = str . split ( ' ' )
2948+ {
2949+ 'node_id' => arr [ 0 ] ,
2950+ 'ip_port' => arr [ 1 ] ,
2951+ 'flags' => arr [ 2 ] . split ( ',' ) ,
2952+ 'master_node_id' => arr [ 3 ] ,
2953+ 'ping_sent' => arr [ 4 ] ,
2954+ 'pong_recv' => arr [ 5 ] ,
2955+ 'config_epoch' => arr [ 6 ] ,
2956+ 'link_state' => arr [ 7 ] ,
2957+ 'slots' => arr [ 8 ] . nil? ? nil : Range . new ( *arr [ 8 ] . split ( '-' ) )
2958+ }
2959+ }
2960+
2961+ HashifyClusterSlots =
2962+ lambda { |reply |
2963+ reply . map do |arr |
2964+ first_slot , last_slot = arr [ 0 ..1 ]
2965+ master = { 'ip' => arr [ 2 ] [ 0 ] , 'port' => arr [ 2 ] [ 1 ] , 'node_id' => arr [ 2 ] [ 2 ] }
2966+ replicas = arr [ 3 ..-1 ] . map { |r | { 'ip' => r [ 0 ] , 'port' => r [ 1 ] , 'node_id' => r [ 2 ] } }
2967+ {
2968+ 'start_slot' => first_slot ,
2969+ 'end_slot' => last_slot ,
2970+ 'master' => master ,
2971+ 'replicas' => replicas
2972+ }
2973+ end
2974+ }
2975+
2976+ HashifyClusterNodes =
2977+ lambda { |reply |
2978+ reply . split ( /[\r \n ]+/ ) . map { |str | HashifyClusterNodeInfo . call ( str ) }
2979+ }
2980+
2981+ HashifyClusterSlaves =
2982+ lambda { |reply |
2983+ reply . map { |str | HashifyClusterNodeInfo . call ( str ) }
2984+ }
2985+
2986+ Noop = -> ( reply ) { reply }
2987+
28992988 def _geoarguments ( *args , options : nil , sort : nil , count : nil )
29002989 args . push sort if sort
29012990 args . push 'count' , count if count
@@ -2918,11 +3007,11 @@ def _subscription(method, timeout, channels, block)
29183007 @client = original
29193008 end
29203009 end
2921-
29223010end
29233011
29243012require_relative "redis/version"
29253013require_relative "redis/connection"
29263014require_relative "redis/client"
3015+ require_relative "redis/cluster"
29273016require_relative "redis/pipeline"
29283017require_relative "redis/subscribe"
0 commit comments