redis 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +46 -1
  4. data/lib/redis/client.rb +29 -12
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +34 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +104 -0
  9. data/lib/redis/cluster/node_key.rb +35 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +77 -0
  12. data/lib/redis/cluster/slot.rb +69 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +286 -0
  15. data/lib/redis/connection/ruby.rb +5 -2
  16. data/lib/redis/distributed.rb +13 -6
  17. data/lib/redis/errors.rb +46 -0
  18. data/lib/redis/pipeline.rb +9 -1
  19. data/lib/redis/version.rb +1 -1
  20. data/lib/redis.rb +692 -25
  21. metadata +27 -184
  22. data/.gitignore +0 -16
  23. data/.travis/Gemfile +0 -13
  24. data/.travis.yml +0 -73
  25. data/.yardopts +0 -3
  26. data/Gemfile +0 -3
  27. data/benchmarking/logging.rb +0 -71
  28. data/benchmarking/pipeline.rb +0 -51
  29. data/benchmarking/speed.rb +0 -21
  30. data/benchmarking/suite.rb +0 -24
  31. data/benchmarking/worker.rb +0 -71
  32. data/bors.toml +0 -14
  33. data/examples/basic.rb +0 -15
  34. data/examples/consistency.rb +0 -114
  35. data/examples/dist_redis.rb +0 -43
  36. data/examples/incr-decr.rb +0 -17
  37. data/examples/list.rb +0 -26
  38. data/examples/pubsub.rb +0 -37
  39. data/examples/sentinel/sentinel.conf +0 -9
  40. data/examples/sentinel/start +0 -49
  41. data/examples/sentinel.rb +0 -41
  42. data/examples/sets.rb +0 -36
  43. data/examples/unicorn/config.ru +0 -3
  44. data/examples/unicorn/unicorn.rb +0 -20
  45. data/makefile +0 -42
  46. data/redis.gemspec +0 -42
  47. data/test/bitpos_test.rb +0 -63
  48. data/test/blocking_commands_test.rb +0 -40
  49. data/test/client_test.rb +0 -59
  50. data/test/command_map_test.rb +0 -28
  51. data/test/commands_on_hashes_test.rb +0 -19
  52. data/test/commands_on_hyper_log_log_test.rb +0 -19
  53. data/test/commands_on_lists_test.rb +0 -18
  54. data/test/commands_on_sets_test.rb +0 -75
  55. data/test/commands_on_sorted_sets_test.rb +0 -150
  56. data/test/commands_on_strings_test.rb +0 -99
  57. data/test/commands_on_value_types_test.rb +0 -171
  58. data/test/connection_handling_test.rb +0 -275
  59. data/test/connection_test.rb +0 -57
  60. data/test/db/.gitkeep +0 -0
  61. data/test/distributed_blocking_commands_test.rb +0 -44
  62. data/test/distributed_commands_on_hashes_test.rb +0 -8
  63. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -31
  64. data/test/distributed_commands_on_lists_test.rb +0 -20
  65. data/test/distributed_commands_on_sets_test.rb +0 -106
  66. data/test/distributed_commands_on_sorted_sets_test.rb +0 -16
  67. data/test/distributed_commands_on_strings_test.rb +0 -69
  68. data/test/distributed_commands_on_value_types_test.rb +0 -93
  69. data/test/distributed_commands_requiring_clustering_test.rb +0 -162
  70. data/test/distributed_connection_handling_test.rb +0 -21
  71. data/test/distributed_internals_test.rb +0 -68
  72. data/test/distributed_key_tags_test.rb +0 -50
  73. data/test/distributed_persistence_control_commands_test.rb +0 -24
  74. data/test/distributed_publish_subscribe_test.rb +0 -90
  75. data/test/distributed_remote_server_control_commands_test.rb +0 -64
  76. data/test/distributed_scripting_test.rb +0 -100
  77. data/test/distributed_sorting_test.rb +0 -18
  78. data/test/distributed_test.rb +0 -56
  79. data/test/distributed_transactions_test.rb +0 -30
  80. data/test/encoding_test.rb +0 -14
  81. data/test/error_replies_test.rb +0 -57
  82. data/test/fork_safety_test.rb +0 -60
  83. data/test/helper.rb +0 -201
  84. data/test/helper_test.rb +0 -22
  85. data/test/internals_test.rb +0 -389
  86. data/test/lint/blocking_commands.rb +0 -150
  87. data/test/lint/hashes.rb +0 -162
  88. data/test/lint/hyper_log_log.rb +0 -60
  89. data/test/lint/lists.rb +0 -143
  90. data/test/lint/sets.rb +0 -140
  91. data/test/lint/sorted_sets.rb +0 -316
  92. data/test/lint/strings.rb +0 -246
  93. data/test/lint/value_types.rb +0 -130
  94. data/test/persistence_control_commands_test.rb +0 -24
  95. data/test/pipelining_commands_test.rb +0 -238
  96. data/test/publish_subscribe_test.rb +0 -280
  97. data/test/remote_server_control_commands_test.rb +0 -175
  98. data/test/scanning_test.rb +0 -407
  99. data/test/scripting_test.rb +0 -76
  100. data/test/sentinel_command_test.rb +0 -78
  101. data/test/sentinel_test.rb +0 -253
  102. data/test/sorting_test.rb +0 -57
  103. data/test/ssl_test.rb +0 -69
  104. data/test/support/connection/hiredis.rb +0 -1
  105. data/test/support/connection/ruby.rb +0 -1
  106. data/test/support/connection/synchrony.rb +0 -17
  107. data/test/support/redis_mock.rb +0 -130
  108. data/test/support/ssl/gen_certs.sh +0 -31
  109. data/test/support/ssl/trusted-ca.crt +0 -25
  110. data/test/support/ssl/trusted-ca.key +0 -27
  111. data/test/support/ssl/trusted-cert.crt +0 -81
  112. data/test/support/ssl/trusted-cert.key +0 -28
  113. data/test/support/ssl/untrusted-ca.crt +0 -26
  114. data/test/support/ssl/untrusted-ca.key +0 -27
  115. data/test/support/ssl/untrusted-cert.crt +0 -82
  116. data/test/support/ssl/untrusted-cert.key +0 -28
  117. data/test/support/wire/synchrony.rb +0 -24
  118. data/test/support/wire/thread.rb +0 -5
  119. data/test/synchrony_driver.rb +0 -85
  120. data/test/test.conf.erb +0 -9
  121. data/test/thread_safety_test.rb +0 -60
  122. data/test/transactions_test.rb +0 -262
  123. data/test/unknown_commands_test.rb +0 -12
  124. data/test/url_param_test.rb +0 -136
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7e0d0ba2328aca583c0f487729779c2b10b12c7b
4
- data.tar.gz: 4a411cfa3512bafe7c2c12ac8a02c02bc87065aa
3
+ metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
4
+ data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
5
5
  SHA512:
6
- metadata.gz: 3af7dc63dc7bdc243da306817583db9b6989c21a0714c6652b4a443bfd9c46b2afba0707b028d708a74148bbeedd09d4ec207a0459b3559cb19b95f2f030bb07
7
- data.tar.gz: 98d5aed456ebcd3ccdc50980879a3c14be4c8a1874c6febae95579d2fb3828bda2e82b05e62dc6a4966e178993d4a2a20dc3dedd9f8583cd731851eab2f8e164
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 Exception => e
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
- if options.include?(:sentinels)
88
- @connector = Connector::Sentinel.new(@options)
89
- else
90
- @connector = Connector.new(@options)
91
- end
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(pipeline.commands)).tap do
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
- result[0] = read
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 + 1] = reply
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