Skip to content

Commit f48af99

Browse files
committed
Add Redis Cluster support
1 parent ddf058b commit f48af99

File tree

62 files changed

+3875
-499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3875
-499
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Gemfile.lock
55
/tmp/
66
/.idea
77
/.yardoc
8+
/.bundle
89
/coverage/*
910
/doc/
1011
/examples/sentinel/sentinel.conf
@@ -14,3 +15,5 @@ Gemfile.lock
1415
/redis/*
1516
/test/db
1617
/test/test.conf
18+
appendonly.aof
19+
temp-rewriteaof-*.aof

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ before_install:
66
- gem update --system 2.6.14
77
- gem --version
88

9-
script: make test
9+
script: make
1010

1111
rvm:
1212
- 2.2.2
@@ -25,7 +25,7 @@ before_script:
2525
env:
2626
global:
2727
- VERBOSE=true
28-
- TIMEOUT=1
28+
- TIMEOUT=9
2929
matrix:
3030
- DRIVER=ruby REDIS_BRANCH=3.0
3131
- DRIVER=ruby REDIS_BRANCH=3.2

lib/redis.rb

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
29223010
end
29233011

29243012
require_relative "redis/version"
29253013
require_relative "redis/connection"
29263014
require_relative "redis/client"
3015+
require_relative "redis/cluster"
29273016
require_relative "redis/pipeline"
29283017
require_relative "redis/subscribe"

0 commit comments

Comments
 (0)