redis 4.0.1 → 4.1.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/CHANGELOG.md +38 -0
- data/README.md +46 -1
- data/lib/redis/client.rb +29 -12
- data/lib/redis/cluster/command.rb +81 -0
- data/lib/redis/cluster/command_loader.rb +34 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +104 -0
- data/lib/redis/cluster/node_key.rb +35 -0
- data/lib/redis/cluster/node_loader.rb +37 -0
- data/lib/redis/cluster/option.rb +77 -0
- data/lib/redis/cluster/slot.rb +69 -0
- data/lib/redis/cluster/slot_loader.rb +49 -0
- data/lib/redis/cluster.rb +286 -0
- data/lib/redis/connection/ruby.rb +5 -2
- data/lib/redis/distributed.rb +13 -6
- data/lib/redis/errors.rb +46 -0
- data/lib/redis/pipeline.rb +9 -1
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +692 -25
- metadata +27 -184
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -13
- data/.travis.yml +0 -73
- data/.yardopts +0 -3
- data/Gemfile +0 -3
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/bors.toml +0 -14
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sentinel.rb +0 -41
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/makefile +0 -42
- data/redis.gemspec +0 -42
- data/test/bitpos_test.rb +0 -63
- data/test/blocking_commands_test.rb +0 -40
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -28
- data/test/commands_on_hashes_test.rb +0 -19
- data/test/commands_on_hyper_log_log_test.rb +0 -19
- data/test/commands_on_lists_test.rb +0 -18
- data/test/commands_on_sets_test.rb +0 -75
- data/test/commands_on_sorted_sets_test.rb +0 -150
- data/test/commands_on_strings_test.rb +0 -99
- data/test/commands_on_value_types_test.rb +0 -171
- data/test/connection_handling_test.rb +0 -275
- data/test/connection_test.rb +0 -57
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -44
- data/test/distributed_commands_on_hashes_test.rb +0 -8
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -31
- data/test/distributed_commands_on_lists_test.rb +0 -20
- data/test/distributed_commands_on_sets_test.rb +0 -106
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -16
- data/test/distributed_commands_on_strings_test.rb +0 -69
- data/test/distributed_commands_on_value_types_test.rb +0 -93
- data/test/distributed_commands_requiring_clustering_test.rb +0 -162
- data/test/distributed_connection_handling_test.rb +0 -21
- data/test/distributed_internals_test.rb +0 -68
- data/test/distributed_key_tags_test.rb +0 -50
- data/test/distributed_persistence_control_commands_test.rb +0 -24
- data/test/distributed_publish_subscribe_test.rb +0 -90
- data/test/distributed_remote_server_control_commands_test.rb +0 -64
- data/test/distributed_scripting_test.rb +0 -100
- data/test/distributed_sorting_test.rb +0 -18
- data/test/distributed_test.rb +0 -56
- data/test/distributed_transactions_test.rb +0 -30
- data/test/encoding_test.rb +0 -14
- data/test/error_replies_test.rb +0 -57
- data/test/fork_safety_test.rb +0 -60
- data/test/helper.rb +0 -201
- data/test/helper_test.rb +0 -22
- data/test/internals_test.rb +0 -389
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -140
- data/test/lint/sorted_sets.rb +0 -316
- data/test/lint/strings.rb +0 -246
- data/test/lint/value_types.rb +0 -130
- data/test/persistence_control_commands_test.rb +0 -24
- data/test/pipelining_commands_test.rb +0 -238
- data/test/publish_subscribe_test.rb +0 -280
- data/test/remote_server_control_commands_test.rb +0 -175
- data/test/scanning_test.rb +0 -407
- data/test/scripting_test.rb +0 -76
- data/test/sentinel_command_test.rb +0 -78
- data/test/sentinel_test.rb +0 -253
- data/test/sorting_test.rb +0 -57
- data/test/ssl_test.rb +0 -69
- data/test/support/connection/hiredis.rb +0 -1
- data/test/support/connection/ruby.rb +0 -1
- data/test/support/connection/synchrony.rb +0 -17
- data/test/support/redis_mock.rb +0 -130
- data/test/support/ssl/gen_certs.sh +0 -31
- data/test/support/ssl/trusted-ca.crt +0 -25
- data/test/support/ssl/trusted-ca.key +0 -27
- data/test/support/ssl/trusted-cert.crt +0 -81
- data/test/support/ssl/trusted-cert.key +0 -28
- data/test/support/ssl/untrusted-ca.crt +0 -26
- data/test/support/ssl/untrusted-ca.key +0 -27
- data/test/support/ssl/untrusted-cert.crt +0 -82
- data/test/support/ssl/untrusted-cert.key +0 -28
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -85
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -60
- data/test/transactions_test.rb +0 -262
- data/test/unknown_commands_test.rb +0 -12
- data/test/url_param_test.rb +0 -136
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
|
4
|
+
data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b39badbb4689a4ea93cbc65ad00f0c967f24dadcc41e7750ab82977176236d6d8609d8e7b74e13a3829ce008c4a5de90930e265f6b6e03af710811419da6ccf6
|
7
|
+
data.tar.gz: 3ede92146cb181328657a4713e94473f86661dffe0a66d23f48edaa2c7a6c52543a7e7036794baa9dcc2945bc3f9e5eebd9192b70510bda1ad9c0d6e67253830
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
# 4.1.0
|
4
|
+
|
5
|
+
* Add Redis Cluster support. See #716.
|
6
|
+
* Add streams support. See #799 and #811.
|
7
|
+
* Add ZPOP* support. See #812.
|
8
|
+
* Fix issues with integer-like objects as BPOP timeout
|
9
|
+
|
10
|
+
# 4.0.3
|
11
|
+
|
12
|
+
* Fix raising command error for first command in pipeline. See #788.
|
13
|
+
* Fix the gemspec to stop exposing a `build` executable. See #785.
|
14
|
+
* Add `:reconnect_delay` and `:reconnect_delay_max` options. See #778.
|
15
|
+
|
16
|
+
# 4.0.2
|
17
|
+
|
18
|
+
* Added `Redis#unlink`. See #766.
|
19
|
+
|
20
|
+
* `Redis.new` now accept a custom connector via `:connector`. See #591.
|
21
|
+
|
22
|
+
* `Redis#multi` no longer perform empty transactions. See #747.
|
23
|
+
|
24
|
+
* `Redis#hdel` now accepts hash keys as multiple arguments like `#del`. See #755.
|
25
|
+
|
26
|
+
* Allow to skip SSL verification. See #745.
|
27
|
+
|
28
|
+
* Add Geo commands: `geoadd`, `geohash`, `georadius`, `georadiusbymember`, `geopos`, `geodist`. See #730.
|
29
|
+
|
1
30
|
# 4.0.1
|
2
31
|
|
3
32
|
* `Redis::Distributed` now supports `mget` and `mapped_mget`. See #687.
|
@@ -21,6 +50,15 @@
|
|
21
50
|
|
22
51
|
* Dropped official support for Ruby < 2.2.2.
|
23
52
|
|
53
|
+
# 3.3.5
|
54
|
+
|
55
|
+
* Fixed Ruby 1.8 compatibility after backporting `Redis#connection`. See #719.
|
56
|
+
|
57
|
+
# 3.3.4 (yanked)
|
58
|
+
|
59
|
+
* `Redis#connection` returns a hash with connection information.
|
60
|
+
You shouldn't need to call `Redis#_client`, ever.
|
61
|
+
|
24
62
|
# 3.3.3
|
25
63
|
|
26
64
|
* Improved timeout handling after dropping Timeout module.
|
data/README.md
CHANGED
@@ -176,7 +176,7 @@ it can't connect to the server a `Redis::CannotConnectError` error will be raise
|
|
176
176
|
```ruby
|
177
177
|
begin
|
178
178
|
redis.ping
|
179
|
-
rescue
|
179
|
+
rescue StandardError => e
|
180
180
|
e.inspect
|
181
181
|
# => #<Redis::CannotConnectError: Timed out connecting to Redis on 10.0.1.1:6380>
|
182
182
|
|
@@ -220,6 +220,51 @@ end
|
|
220
220
|
|
221
221
|
If no message is received after 5 seconds, the client will unsubscribe.
|
222
222
|
|
223
|
+
## Reconnections
|
224
|
+
|
225
|
+
The client allows you to configure how many `reconnect_attempts` it should
|
226
|
+
complete before declaring a connection as failed. Furthermore, you may want
|
227
|
+
to control the maximum duration between reconnection attempts with
|
228
|
+
`reconnect_delay` and `reconnect_delay_max`.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Redis.new(
|
232
|
+
:reconnect_attempts => 10,
|
233
|
+
:reconnect_delay => 1.5,
|
234
|
+
:reconnect_delay_max => 10.0,
|
235
|
+
)
|
236
|
+
```
|
237
|
+
|
238
|
+
The delay values are specified in seconds. With the above configuration, the
|
239
|
+
client would attempt 10 reconnections, exponentially increasing the duration
|
240
|
+
between each attempt but it never waits longer than `reconnect_delay_max`.
|
241
|
+
|
242
|
+
This is the retry algorithm:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min
|
246
|
+
```
|
247
|
+
|
248
|
+
**By default**, this gem will only **retry a connection once** and then fail, but with the
|
249
|
+
above configuration the reconnection attempt would look like this:
|
250
|
+
|
251
|
+
#|Attempt wait time|Total wait time
|
252
|
+
:-:|:-:|:-:
|
253
|
+
1|1.5s|1.5s
|
254
|
+
2|3.0s|4.5s
|
255
|
+
3|6.0s|10.5s
|
256
|
+
4|10.0s|20.5s
|
257
|
+
5|10.0s|30.5s
|
258
|
+
6|10.0s|40.5s
|
259
|
+
7|10.0s|50.5s
|
260
|
+
8|10.0s|60.5s
|
261
|
+
9|10.0s|70.5s
|
262
|
+
10|10.0s|80.5s
|
263
|
+
|
264
|
+
So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying
|
265
|
+
to reconnect, this is likely fine in long-running background processes, but if
|
266
|
+
you use Redis to drive your website you might want to have a lower
|
267
|
+
`reconnect_delay_max` or have less `reconnect_attempts`.
|
223
268
|
|
224
269
|
## SSL/TLS Support
|
225
270
|
|
data/lib/redis/client.rb
CHANGED
@@ -18,6 +18,8 @@ class Redis
|
|
18
18
|
:id => nil,
|
19
19
|
:tcp_keepalive => 0,
|
20
20
|
:reconnect_attempts => 1,
|
21
|
+
:reconnect_delay => 0,
|
22
|
+
:reconnect_delay_max => 0.5,
|
21
23
|
:inherit_socket => false
|
22
24
|
}
|
23
25
|
|
@@ -84,11 +86,14 @@ class Redis
|
|
84
86
|
|
85
87
|
@pending_reads = 0
|
86
88
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
@connector =
|
90
|
+
if options.include?(:sentinels)
|
91
|
+
Connector::Sentinel.new(@options)
|
92
|
+
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
93
|
+
options.delete(:connector).new(@options)
|
94
|
+
else
|
95
|
+
Connector.new(@options)
|
96
|
+
end
|
92
97
|
end
|
93
98
|
|
94
99
|
def connect
|
@@ -150,9 +155,12 @@ class Redis
|
|
150
155
|
end
|
151
156
|
|
152
157
|
def call_pipeline(pipeline)
|
158
|
+
commands = pipeline.commands
|
159
|
+
return [] if commands.empty?
|
160
|
+
|
153
161
|
with_reconnect pipeline.with_reconnect? do
|
154
162
|
begin
|
155
|
-
pipeline.finish(call_pipelined(
|
163
|
+
pipeline.finish(call_pipelined(commands)).tap do
|
156
164
|
self.db = pipeline.db if pipeline.db
|
157
165
|
end
|
158
166
|
rescue ConnectionError => e
|
@@ -183,13 +191,10 @@ class Redis
|
|
183
191
|
exception = nil
|
184
192
|
|
185
193
|
process(commands) do
|
186
|
-
|
187
|
-
|
188
|
-
@reconnect = false
|
189
|
-
|
190
|
-
(commands.size - 1).times do |i|
|
194
|
+
commands.size.times do |i|
|
191
195
|
reply = read
|
192
|
-
result[i
|
196
|
+
result[i] = reply
|
197
|
+
@reconnect = false
|
193
198
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
194
199
|
end
|
195
200
|
end
|
@@ -272,12 +277,15 @@ class Redis
|
|
272
277
|
|
273
278
|
def with_socket_timeout(timeout)
|
274
279
|
connect unless connected?
|
280
|
+
original = @options[:read_timeout]
|
275
281
|
|
276
282
|
begin
|
277
283
|
connection.timeout = timeout
|
284
|
+
@options[:read_timeout] = timeout # for reconnection
|
278
285
|
yield
|
279
286
|
ensure
|
280
287
|
connection.timeout = self.timeout if connected?
|
288
|
+
@options[:read_timeout] = original
|
281
289
|
end
|
282
290
|
end
|
283
291
|
|
@@ -334,6 +342,7 @@ class Redis
|
|
334
342
|
@connection = @options[:driver].connect(@options)
|
335
343
|
@pending_reads = 0
|
336
344
|
rescue TimeoutError,
|
345
|
+
SocketError,
|
337
346
|
Errno::ECONNREFUSED,
|
338
347
|
Errno::EHOSTDOWN,
|
339
348
|
Errno::EHOSTUNREACH,
|
@@ -368,6 +377,10 @@ class Redis
|
|
368
377
|
disconnect
|
369
378
|
|
370
379
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
380
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
381
|
+
@options[:reconnect_delay_max]].min
|
382
|
+
|
383
|
+
Kernel.sleep(sleep_t)
|
371
384
|
retry
|
372
385
|
else
|
373
386
|
raise
|
@@ -444,6 +457,10 @@ class Redis
|
|
444
457
|
options[:read_timeout] = Float(options[:read_timeout])
|
445
458
|
options[:write_timeout] = Float(options[:write_timeout])
|
446
459
|
|
460
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
461
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
462
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
463
|
+
|
447
464
|
options[:db] = options[:db].to_i
|
448
465
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
449
466
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Keep details about Redis commands for Redis Cluster Client.
|
8
|
+
# @see https://redis.io/commands/command
|
9
|
+
class Command
|
10
|
+
def initialize(details)
|
11
|
+
@details = pick_details(details)
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_first_key(command)
|
15
|
+
i = determine_first_key_position(command)
|
16
|
+
return '' if i == 0
|
17
|
+
|
18
|
+
key = command[i].to_s
|
19
|
+
hash_tag = extract_hash_tag(key)
|
20
|
+
hash_tag.empty? ? key : hash_tag
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_send_to_master?(command)
|
24
|
+
dig_details(command, :write)
|
25
|
+
end
|
26
|
+
|
27
|
+
def should_send_to_slave?(command)
|
28
|
+
dig_details(command, :readonly)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def pick_details(details)
|
34
|
+
details.map do |command, detail|
|
35
|
+
[command, {
|
36
|
+
first_key_position: detail[:first],
|
37
|
+
write: detail[:flags].include?('write'),
|
38
|
+
readonly: detail[:flags].include?('readonly')
|
39
|
+
}]
|
40
|
+
end.to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
def dig_details(command, key)
|
44
|
+
name = command.first.to_s
|
45
|
+
return unless @details.key?(name)
|
46
|
+
|
47
|
+
@details.fetch(name).fetch(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def determine_first_key_position(command)
|
51
|
+
case command.first.to_s.downcase
|
52
|
+
when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
|
53
|
+
when 'object' then 2
|
54
|
+
when 'memory'
|
55
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
56
|
+
when 'scan', 'sscan', 'hscan', 'zscan'
|
57
|
+
determine_optional_key_position(command, 'match')
|
58
|
+
when 'xread', 'xreadgroup'
|
59
|
+
determine_optional_key_position(command, 'streams')
|
60
|
+
else
|
61
|
+
dig_details(command, :first_key_position).to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def determine_optional_key_position(command, option_name)
|
66
|
+
idx = command.map(&:to_s).map(&:downcase).index(option_name)
|
67
|
+
idx.nil? ? 0 : idx + 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
|
71
|
+
def extract_hash_tag(key)
|
72
|
+
s = key.index('{')
|
73
|
+
e = key.index('}', s.to_i + 1)
|
74
|
+
|
75
|
+
return '' if s.nil? || e.nil?
|
76
|
+
|
77
|
+
key[s + 1..e - 1]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Load details about Redis commands for Redis Cluster Client
|
8
|
+
# @see https://redis.io/commands/command
|
9
|
+
module CommandLoader
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def load(nodes)
|
13
|
+
details = {}
|
14
|
+
|
15
|
+
nodes.each do |node|
|
16
|
+
details = fetch_command_details(node)
|
17
|
+
details.empty? ? next : break
|
18
|
+
end
|
19
|
+
|
20
|
+
details
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_command_details(node)
|
24
|
+
node.call(%i[command]).map do |reply|
|
25
|
+
[reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
|
26
|
+
end.to_h
|
27
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
28
|
+
{} # can retry on another node
|
29
|
+
end
|
30
|
+
|
31
|
+
private_class_method :fetch_command_details
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Key to slot converter for Redis Cluster Client
|
6
|
+
#
|
7
|
+
# We can test it by `CLUSTER KEYSLOT` command.
|
8
|
+
#
|
9
|
+
# @see https://github.com/antirez/redis-rb-cluster
|
10
|
+
# Reference implementation in Ruby
|
11
|
+
# @see https://redis.io/topics/cluster-spec#appendix
|
12
|
+
# Reference implementation in ANSI C
|
13
|
+
# @see https://redis.io/commands/cluster-keyslot
|
14
|
+
# CLUSTER KEYSLOT command reference
|
15
|
+
#
|
16
|
+
# Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
|
17
|
+
module KeySlotConverter
|
18
|
+
XMODEM_CRC16_LOOKUP = [
|
19
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
20
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
21
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
22
|
+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
23
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
24
|
+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
25
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
26
|
+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
27
|
+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
28
|
+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
29
|
+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
30
|
+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
31
|
+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
32
|
+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
33
|
+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
34
|
+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
35
|
+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
36
|
+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
37
|
+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
38
|
+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
39
|
+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
40
|
+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
41
|
+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
42
|
+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
43
|
+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
44
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
45
|
+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
46
|
+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
47
|
+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
48
|
+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
49
|
+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
50
|
+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
51
|
+
].freeze
|
52
|
+
|
53
|
+
HASH_SLOTS = 16_384
|
54
|
+
|
55
|
+
module_function
|
56
|
+
|
57
|
+
# Convert key into slot.
|
58
|
+
#
|
59
|
+
# @param key [String] the key of the redis command
|
60
|
+
#
|
61
|
+
# @return [Integer] slot number
|
62
|
+
def convert(key)
|
63
|
+
crc = 0
|
64
|
+
key.each_byte do |b|
|
65
|
+
crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
|
66
|
+
end
|
67
|
+
|
68
|
+
crc % HASH_SLOTS
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Keep client list of node for Redis Cluster Client
|
8
|
+
class Node
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
ReloadNeeded = Class.new(StandardError)
|
12
|
+
|
13
|
+
ROLE_SLAVE = 'slave'
|
14
|
+
|
15
|
+
def initialize(options, node_flags = {}, with_replica = false)
|
16
|
+
@with_replica = with_replica
|
17
|
+
@node_flags = node_flags
|
18
|
+
@clients = build_clients(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
@clients.values.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def sample
|
26
|
+
@clients.values.sample
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by(node_key)
|
30
|
+
@clients.fetch(node_key)
|
31
|
+
rescue KeyError
|
32
|
+
raise ReloadNeeded
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_all(command, &block)
|
36
|
+
try_map { |_, client| client.call(command, &block) }.values
|
37
|
+
end
|
38
|
+
|
39
|
+
def call_master(command, &block)
|
40
|
+
try_map do |node_key, client|
|
41
|
+
next if slave?(node_key)
|
42
|
+
client.call(command, &block)
|
43
|
+
end.values
|
44
|
+
end
|
45
|
+
|
46
|
+
def call_slave(command, &block)
|
47
|
+
return call_master(command, &block) if replica_disabled?
|
48
|
+
|
49
|
+
try_map do |node_key, client|
|
50
|
+
next if master?(node_key)
|
51
|
+
client.call(command, &block)
|
52
|
+
end.values
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_all(commands, &block)
|
56
|
+
try_map { |_, client| client.process(commands, &block) }.values
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def replica_disabled?
|
62
|
+
!@with_replica
|
63
|
+
end
|
64
|
+
|
65
|
+
def master?(node_key)
|
66
|
+
!slave?(node_key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def slave?(node_key)
|
70
|
+
@node_flags[node_key] == ROLE_SLAVE
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_clients(options)
|
74
|
+
clients = options.map do |node_key, option|
|
75
|
+
next if replica_disabled? && slave?(node_key)
|
76
|
+
|
77
|
+
client = Client.new(option)
|
78
|
+
client.call(%i[readonly]) if slave?(node_key)
|
79
|
+
[node_key, client]
|
80
|
+
end
|
81
|
+
|
82
|
+
clients.compact.to_h
|
83
|
+
end
|
84
|
+
|
85
|
+
def try_map
|
86
|
+
errors = {}
|
87
|
+
results = {}
|
88
|
+
|
89
|
+
@clients.each do |node_key, client|
|
90
|
+
begin
|
91
|
+
reply = yield(node_key, client)
|
92
|
+
results[node_key] = reply unless reply.nil?
|
93
|
+
rescue CommandError => err
|
94
|
+
errors[node_key] = err
|
95
|
+
next
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
return results if errors.empty?
|
100
|
+
raise CommandErrorCollection, errors
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Node key's format is `<ip>:<port>`.
|
6
|
+
# It is different from node id.
|
7
|
+
# Node id is internal identifying code in Redis Cluster.
|
8
|
+
module NodeKey
|
9
|
+
DEFAULT_SCHEME = 'redis'
|
10
|
+
SECURE_SCHEME = 'rediss'
|
11
|
+
DELIMITER = ':'
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def to_node_urls(node_keys, secure:)
|
16
|
+
scheme = secure ? SECURE_SCHEME : DEFAULT_SCHEME
|
17
|
+
node_keys
|
18
|
+
.map { |k| k.split(DELIMITER) }
|
19
|
+
.map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
|
20
|
+
end
|
21
|
+
|
22
|
+
def split(node_key)
|
23
|
+
node_key.split(DELIMITER)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_from_uri(uri)
|
27
|
+
"#{uri.host}#{DELIMITER}#{uri.port}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_from_host_port(host, port)
|
31
|
+
"#{host}#{DELIMITER}#{port}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Load and hashify node info for Redis Cluster Client
|
8
|
+
module NodeLoader
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def load_flags(nodes)
|
12
|
+
info = {}
|
13
|
+
|
14
|
+
nodes.each do |node|
|
15
|
+
info = fetch_node_info(node)
|
16
|
+
info.empty? ? next : break
|
17
|
+
end
|
18
|
+
|
19
|
+
return info unless info.empty?
|
20
|
+
|
21
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_node_info(node)
|
25
|
+
node.call(%i[cluster nodes])
|
26
|
+
.split("\n")
|
27
|
+
.map { |str| str.split(' ') }
|
28
|
+
.map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
|
29
|
+
.to_h
|
30
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
31
|
+
{} # can retry on another node
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method :fetch_node_info
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
require_relative 'node_key'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Redis
|
8
|
+
class Cluster
|
9
|
+
# Keep options for Redis Cluster Client
|
10
|
+
class Option
|
11
|
+
DEFAULT_SCHEME = 'redis'
|
12
|
+
SECURE_SCHEME = 'rediss'
|
13
|
+
VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
options = options.dup
|
17
|
+
node_addrs = options.delete(:cluster)
|
18
|
+
@node_uris = build_node_uris(node_addrs)
|
19
|
+
@replica = options.delete(:replica) == true
|
20
|
+
@options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def per_node_key
|
24
|
+
@node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
|
25
|
+
.to_h
|
26
|
+
end
|
27
|
+
|
28
|
+
def secure?
|
29
|
+
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
|
30
|
+
end
|
31
|
+
|
32
|
+
def use_replica?
|
33
|
+
@replica
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_node(addrs)
|
37
|
+
@node_uris = build_node_uris(addrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_node(host, port)
|
41
|
+
@node_uris << parse_node_hash(host: host, port: port)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def build_node_uris(addrs)
|
47
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
48
|
+
addrs.map { |addr| parse_node_addr(addr) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_node_addr(addr)
|
52
|
+
case addr
|
53
|
+
when String
|
54
|
+
parse_node_url(addr)
|
55
|
+
when Hash
|
56
|
+
parse_node_hash(addr)
|
57
|
+
else
|
58
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_node_url(addr)
|
63
|
+
uri = URI(addr)
|
64
|
+
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
|
+
uri
|
66
|
+
rescue URI::InvalidURIError => err
|
67
|
+
raise InvalidClientOptionError, err.message
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_node_hash(addr)
|
71
|
+
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
|
73
|
+
URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|