redis_cluster 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +8 -0
- data/README.md +5 -7
- data/lib/redis_cluster/client.rb +120 -30
- data/lib/redis_cluster/configuration.rb +4 -1
- data/lib/redis_cluster/errors.rb +22 -3
- data/lib/redis_cluster/pool.rb +9 -3
- data/lib/redis_cluster/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4e3f46f9197fb5e41afea6d2b4e0f41af7defbc
|
|
4
|
+
data.tar.gz: 21e8e6613f409e4d2ea6942cac0c5a44dc855bff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d01fd2610f8e69cec1344105227b0ae007aa60fd066d4f5bc9d77320dbd6eb0b8af6bd524bc4fb9771a3a125cf43b686c4838ac3ec85d414dd7aa9d41a1fdab3
|
|
7
|
+
data.tar.gz: e8006dabb95bfc9ff141fdf0b8120b308b649e65f3183fe6400ff77c4b655f00d91db5ceb5e4131ebfa48d343afe9dc1ca2f2382db0fb77a84acf379626ef3cf
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -28,13 +28,15 @@ Or install it yourself as:
|
|
|
28
28
|
First you need to configure redis cluster with some nodes! Please see: [https://redis.io/topics/cluster-tutorial](https://redis.io/topics/cluster-tutorial)
|
|
29
29
|
|
|
30
30
|
```ruby
|
|
31
|
-
#
|
|
31
|
+
# Don't need all, gem can auto detect all nodes, and process failover if some master nodes down
|
|
32
32
|
hosts = [{host: '127.0.0.1', port: 7000}, {host: '127.0.0.1', port: 7001}]
|
|
33
33
|
rs = RedisCluster.new hosts
|
|
34
34
|
rs.set "test", 1
|
|
35
35
|
rs.get "test"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
At development environment with single redis node, you can set hosts a hash value: {host: 'xx', port: 6379}.
|
|
39
|
+
|
|
38
40
|
If masterauth & requirepass configed, you can initialize below:
|
|
39
41
|
```ruby
|
|
40
42
|
RedisCluster.new hosts, password: 'password'
|
|
@@ -74,20 +76,16 @@ rs.eval "return 'hello redis!'", [:foo]
|
|
|
74
76
|
|
|
75
77
|
## Benchmark test
|
|
76
78
|
|
|
77
|
-
A simple benchmark at my macbook, start 4 master nodes (and 4 cold slave nodes), running with one ruby process.
|
|
78
|
-
This only testing redis_cluster can work, not for redis Performance. When I fork 8 ruby process same time and run get command,redis can run 80,000 - 110,000 times per second at my macbook.
|
|
79
|
-
|
|
80
|
-
|
|
81
79
|
```ruby
|
|
82
80
|
Benchmark.bm do |x|
|
|
83
81
|
x.report do
|
|
84
82
|
1.upto(100_000).each do |i|
|
|
85
|
-
redis.
|
|
83
|
+
redis.set "test#{i}", i
|
|
86
84
|
end
|
|
87
85
|
end
|
|
88
86
|
x.report do
|
|
89
87
|
1.upto(100_000).each do |i|
|
|
90
|
-
redis.
|
|
88
|
+
redis.get "test#{i}"
|
|
91
89
|
end
|
|
92
90
|
end
|
|
93
91
|
x.report do
|
data/lib/redis_cluster/client.rb
CHANGED
|
@@ -4,40 +4,77 @@ module RedisCluster
|
|
|
4
4
|
|
|
5
5
|
class Client
|
|
6
6
|
|
|
7
|
-
def initialize(startup_hosts,
|
|
7
|
+
def initialize(startup_hosts, configs = {})
|
|
8
8
|
@startup_hosts = startup_hosts
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
# Extract configuration options relevant to Redis Cluster.
|
|
11
|
+
|
|
12
|
+
# force_cluster defaults to true to match the client's behavior before
|
|
13
|
+
# the option existed
|
|
14
|
+
@force_cluster = configs.delete(:force_cluster) { |_key| true }
|
|
15
|
+
|
|
16
|
+
# The number of times to retry a failed execute. Redis errors, `MOVE`, or
|
|
17
|
+
# `ASK` are all considered failures that will count towards this tally. A
|
|
18
|
+
# count of at least 2 is probably sensible because if a node disappears
|
|
19
|
+
# the first try will be a Redis error, the second try retry will probably
|
|
20
|
+
# be a `MOVE` (whereupon the node pool is reloaded), and it will take
|
|
21
|
+
# until the third try to succeed.
|
|
22
|
+
@retry_count = configs.delete(:retry_count) { |_key| 3 }
|
|
23
|
+
|
|
24
|
+
# Any leftover configuration goes through to the pool and onto individual
|
|
25
|
+
# Redis clients.
|
|
26
|
+
@pool = Pool.new(configs)
|
|
10
27
|
@mutex = Mutex.new
|
|
28
|
+
|
|
11
29
|
reload_pool_nodes(true)
|
|
12
30
|
end
|
|
13
31
|
|
|
14
32
|
def execute(method, args, &block)
|
|
15
|
-
ttl = Configuration::REQUEST_TTL
|
|
16
33
|
asking = false
|
|
34
|
+
last_error = nil
|
|
35
|
+
retries_left = @retry_count
|
|
17
36
|
try_random_node = false
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
# We use `>= 0` instead of `> 0` because we decrement this counter on the
|
|
39
|
+
# first run.
|
|
40
|
+
while retries_left >= 0
|
|
41
|
+
retries_left -= 1
|
|
42
|
+
|
|
21
43
|
begin
|
|
22
44
|
return @pool.execute(method, args, {asking: asking, random_node: try_random_node}, &block)
|
|
23
|
-
|
|
45
|
+
|
|
46
|
+
rescue Errno::ECONNREFUSED, Redis::TimeoutError, Redis::CannotConnectError, Errno::EACCES => e
|
|
47
|
+
last_error = e
|
|
48
|
+
|
|
49
|
+
# Getting an error while executing may be an indication that we've
|
|
50
|
+
# lost the node that we were talking to and in that case it makes
|
|
51
|
+
# sense to try a different node and maybe reload our node pool (if
|
|
52
|
+
# the new node issues a `MOVE`).
|
|
24
53
|
try_random_node = true
|
|
25
|
-
|
|
54
|
+
|
|
26
55
|
rescue => e
|
|
56
|
+
last_error = e
|
|
57
|
+
|
|
27
58
|
err_code = e.to_s.split.first
|
|
28
59
|
raise e unless %w(MOVED ASK).include?(err_code)
|
|
29
60
|
|
|
30
61
|
if err_code == 'ASK'
|
|
31
62
|
asking = true
|
|
32
63
|
else
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
# `MOVED` indicates a permanent redirect which means that our slot
|
|
65
|
+
# mappings are stale: reload them.
|
|
66
|
+
reload_pool_nodes(false)
|
|
35
67
|
end
|
|
36
68
|
end
|
|
37
69
|
end
|
|
70
|
+
|
|
71
|
+
# If we ran out of retries (the maximum number may have been set to 0),
|
|
72
|
+
# surface any error that was thrown back to the caller. We'd otherwise
|
|
73
|
+
# suppress the error, which would return something quite unexpected.
|
|
74
|
+
raise last_error
|
|
38
75
|
end
|
|
39
76
|
|
|
40
|
-
Configuration
|
|
77
|
+
Configuration.method_names.each do |method_name|
|
|
41
78
|
define_method method_name do |*args, &block|
|
|
42
79
|
execute(method_name, args, &block)
|
|
43
80
|
end
|
|
@@ -49,33 +86,86 @@ module RedisCluster
|
|
|
49
86
|
|
|
50
87
|
private
|
|
51
88
|
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
# Adds only a single node to the client pool and sets it result for the
|
|
90
|
+
# entire space of slots. This is useful when running either a standalone
|
|
91
|
+
# Redis or a single-node Redis Cluster.
|
|
92
|
+
def create_single_node_pool
|
|
93
|
+
host = @startup_hosts
|
|
94
|
+
if host.is_a?(Array)
|
|
95
|
+
if host.length > 1
|
|
96
|
+
raise ArgumentError, "Can only create single node pool for single host"
|
|
97
|
+
end
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
# Flatten the configured host so that we can easily add it to the
|
|
100
|
+
# client pool.
|
|
101
|
+
host = host.first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@pool.add_node!(host, [(0..Configuration::HASH_SLOTS)])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def create_multi_node_pool(raise_error)
|
|
108
|
+
unless @startup_hosts.is_a?(Array)
|
|
109
|
+
raise ArgumentError, "Can only create multi-node pool for multiple hosts"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@startup_hosts.each do |options|
|
|
113
|
+
begin
|
|
114
|
+
redis = Node.redis(@pool.global_configs.merge(options))
|
|
115
|
+
slots_mapping = redis.cluster("slots").group_by{|x| x[2]}
|
|
116
|
+
@pool.delete_except!(slots_mapping.keys)
|
|
117
|
+
slots_mapping.each do |host, infos|
|
|
118
|
+
slots_ranges = infos.map {|x| x[0]..x[1] }
|
|
119
|
+
@pool.add_node!({host: host[0], port: host[1]}, slots_ranges)
|
|
120
|
+
end
|
|
121
|
+
rescue Redis::CommandError => e
|
|
122
|
+
if e.message =~ /cluster\ support\ disabled$/
|
|
123
|
+
if !@force_cluster
|
|
124
|
+
# We're running outside of cluster-mode -- just create a
|
|
125
|
+
# single-node pool and move on. The exception is if we've been
|
|
126
|
+
# asked for force Redis Cluster, in which case we assume this is
|
|
127
|
+
# a configuration problem and maybe raise an error.
|
|
128
|
+
create_single_node_pool
|
|
129
|
+
return
|
|
130
|
+
elsif raise_error
|
|
131
|
+
raise e
|
|
64
132
|
end
|
|
65
|
-
rescue Redis::CommandError => e
|
|
66
|
-
raise e if raise_error && e.message =~ /cluster\ support\ disabled$/
|
|
67
|
-
raise e if e.message =~ /NOAUTH\ Authentication\ required/
|
|
68
|
-
next
|
|
69
|
-
rescue
|
|
70
|
-
next
|
|
71
133
|
end
|
|
72
|
-
|
|
134
|
+
|
|
135
|
+
raise e if e.message =~ /NOAUTH\ Authentication\ required/
|
|
136
|
+
|
|
137
|
+
# TODO: log error for visibility
|
|
138
|
+
next
|
|
139
|
+
rescue
|
|
140
|
+
# TODO: log error for visibility
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# We only need to see a `CLUSTER SLOTS` result from a single host, so
|
|
145
|
+
# break after one success.
|
|
146
|
+
break
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Reloads the client node pool by requesting new information with `CLUSTER
|
|
151
|
+
# SLOTS` or just adding a node directly if running on standalone. Clients
|
|
152
|
+
# are "upserted" so that we don't necessarily drop clients that are still
|
|
153
|
+
# relevant.
|
|
154
|
+
def reload_pool_nodes(raise_error)
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
if @startup_hosts.is_a?(Array)
|
|
157
|
+
create_multi_node_pool(raise_error)
|
|
158
|
+
refresh_startup_nodes
|
|
159
|
+
else
|
|
160
|
+
create_single_node_pool
|
|
73
161
|
end
|
|
74
|
-
fresh_startup_nodes
|
|
75
162
|
end
|
|
76
163
|
end
|
|
77
164
|
|
|
78
|
-
|
|
165
|
+
# Refreshes the contents of @startup_hosts based on the hosts currently in
|
|
166
|
+
# the client pool. This is useful because we may have been told about new
|
|
167
|
+
# hosts after running `CLUSTER SLOTS`.
|
|
168
|
+
def refresh_startup_nodes
|
|
79
169
|
@pool.nodes.each {|node| @startup_hosts.push(node.host_hash) }
|
|
80
170
|
@startup_hosts.uniq!
|
|
81
171
|
end
|
|
@@ -2,7 +2,6 @@ module RedisCluster
|
|
|
2
2
|
|
|
3
3
|
class Configuration
|
|
4
4
|
HASH_SLOTS = 16384
|
|
5
|
-
REQUEST_TTL = 16
|
|
6
5
|
DEFAULT_TIMEOUT = 1
|
|
7
6
|
|
|
8
7
|
SUPPORT_SINGLE_NODE_METHODS = %w(
|
|
@@ -18,6 +17,10 @@ module RedisCluster
|
|
|
18
17
|
).freeze
|
|
19
18
|
|
|
20
19
|
SUPPORT_MULTI_NODE_METHODS = %w(keys script multi pipelined).freeze
|
|
20
|
+
|
|
21
|
+
def self.method_names
|
|
22
|
+
SUPPORT_SINGLE_NODE_METHODS + SUPPORT_MULTI_NODE_METHODS
|
|
23
|
+
end
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
end
|
data/lib/redis_cluster/errors.rb
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
module RedisCluster
|
|
2
|
-
|
|
2
|
+
class CommandNotSupportedError < StandardError
|
|
3
|
+
def initialize(command)
|
|
4
|
+
super("Command #{command} is not supported for Redis Cluster")
|
|
5
|
+
end
|
|
6
|
+
end
|
|
3
7
|
|
|
4
|
-
KeysNotAtSameSlotError
|
|
8
|
+
class KeysNotAtSameSlotError < StandardError
|
|
9
|
+
def initialize(keys)
|
|
10
|
+
super("Keys must map to the same Redis Cluster slot when using " \
|
|
11
|
+
"EVAL/EVALSHA. Consider using Redis Cluster 'hash tags' (see " \
|
|
12
|
+
"documentation). Keys: #{keys}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
5
15
|
|
|
6
|
-
|
|
16
|
+
class KeysNotSpecifiedError < StandardError
|
|
17
|
+
def initialize(command)
|
|
18
|
+
super("Keys must be specified for command #{command}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# These error classes were renamed. These aliases are here for backwards
|
|
23
|
+
# compatibility.
|
|
24
|
+
KeyNotAppointError = KeysNotSpecifiedError
|
|
25
|
+
NotSupportError = CommandNotSupportedError
|
|
7
26
|
end
|
data/lib/redis_cluster/pool.rb
CHANGED
|
@@ -28,7 +28,7 @@ module RedisCluster
|
|
|
28
28
|
return send(method, args, &block) if Configuration::SUPPORT_MULTI_NODE_METHODS.include?(method.to_s)
|
|
29
29
|
|
|
30
30
|
key = key_by_command(method, args)
|
|
31
|
-
raise
|
|
31
|
+
raise CommandNotSupportedError.new(method.upcase) if key.nil?
|
|
32
32
|
|
|
33
33
|
node = other_options[:random_node] ? random_node : node_by(key)
|
|
34
34
|
node.asking if other_options[:asking]
|
|
@@ -70,8 +70,14 @@ module RedisCluster
|
|
|
70
70
|
when 'info', 'exec', 'slaveof', 'config', 'shutdown'
|
|
71
71
|
nil
|
|
72
72
|
when 'eval', 'evalsha'
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if args[1].nil? || args[1].empty?
|
|
74
|
+
raise KeysNotSpecifiedError.new(method.upcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
unless Slot.at_one?(args[1])
|
|
78
|
+
raise KeysNotAtSameSlotError.new(args[1])
|
|
79
|
+
end
|
|
80
|
+
|
|
75
81
|
return args[1][0]
|
|
76
82
|
else
|
|
77
83
|
return args.first
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis_cluster
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- wangzc
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2018-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -142,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
142
142
|
version: '0'
|
|
143
143
|
requirements: []
|
|
144
144
|
rubyforge_project:
|
|
145
|
-
rubygems_version: 2.6.
|
|
145
|
+
rubygems_version: 2.6.13
|
|
146
146
|
signing_key:
|
|
147
147
|
specification_version: 4
|
|
148
148
|
summary: redis cluster client
|