redis 4.0.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +17 -29
  4. data/.travis/Gemfile +5 -0
  5. data/CHANGELOG.md +29 -0
  6. data/Gemfile +5 -0
  7. data/README.md +1 -1
  8. data/bin/build +71 -0
  9. data/lib/redis.rb +198 -12
  10. data/lib/redis/client.rb +26 -12
  11. data/lib/redis/cluster.rb +285 -0
  12. data/lib/redis/cluster/command.rb +81 -0
  13. data/lib/redis/cluster/command_loader.rb +32 -0
  14. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  15. data/lib/redis/cluster/node.rb +104 -0
  16. data/lib/redis/cluster/node_key.rb +35 -0
  17. data/lib/redis/cluster/node_loader.rb +35 -0
  18. data/lib/redis/cluster/option.rb +76 -0
  19. data/lib/redis/cluster/slot.rb +69 -0
  20. data/lib/redis/cluster/slot_loader.rb +47 -0
  21. data/lib/redis/connection/ruby.rb +5 -2
  22. data/lib/redis/distributed.rb +10 -2
  23. data/lib/redis/errors.rb +46 -0
  24. data/lib/redis/pipeline.rb +9 -1
  25. data/lib/redis/version.rb +1 -1
  26. data/makefile +54 -22
  27. data/redis.gemspec +2 -1
  28. data/test/client_test.rb +17 -0
  29. data/test/cluster_abnormal_state_test.rb +38 -0
  30. data/test/cluster_blocking_commands_test.rb +15 -0
  31. data/test/cluster_client_internals_test.rb +77 -0
  32. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  33. data/test/cluster_client_options_test.rb +147 -0
  34. data/test/cluster_client_pipelining_test.rb +59 -0
  35. data/test/cluster_client_replicas_test.rb +36 -0
  36. data/test/cluster_client_slots_test.rb +94 -0
  37. data/test/cluster_client_transactions_test.rb +71 -0
  38. data/test/cluster_commands_on_cluster_test.rb +165 -0
  39. data/test/cluster_commands_on_connection_test.rb +40 -0
  40. data/test/cluster_commands_on_geo_test.rb +74 -0
  41. data/test/cluster_commands_on_hashes_test.rb +11 -0
  42. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  43. data/test/cluster_commands_on_keys_test.rb +134 -0
  44. data/test/cluster_commands_on_lists_test.rb +15 -0
  45. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  46. data/test/cluster_commands_on_scripting_test.rb +56 -0
  47. data/test/cluster_commands_on_server_test.rb +221 -0
  48. data/test/cluster_commands_on_sets_test.rb +39 -0
  49. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  50. data/test/cluster_commands_on_streams_test.rb +196 -0
  51. data/test/cluster_commands_on_strings_test.rb +15 -0
  52. data/test/cluster_commands_on_transactions_test.rb +41 -0
  53. data/test/cluster_commands_on_value_types_test.rb +14 -0
  54. data/test/commands_on_geo_test.rb +116 -0
  55. data/test/commands_on_hashes_test.rb +2 -14
  56. data/test/commands_on_hyper_log_log_test.rb +2 -14
  57. data/test/commands_on_lists_test.rb +2 -13
  58. data/test/commands_on_sets_test.rb +2 -70
  59. data/test/commands_on_sorted_sets_test.rb +2 -145
  60. data/test/commands_on_strings_test.rb +2 -94
  61. data/test/commands_on_value_types_test.rb +36 -0
  62. data/test/distributed_blocking_commands_test.rb +8 -0
  63. data/test/distributed_commands_on_hashes_test.rb +16 -3
  64. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -13
  65. data/test/distributed_commands_on_lists_test.rb +4 -5
  66. data/test/distributed_commands_on_sets_test.rb +45 -46
  67. data/test/distributed_commands_on_sorted_sets_test.rb +51 -8
  68. data/test/distributed_commands_on_strings_test.rb +10 -0
  69. data/test/distributed_commands_on_value_types_test.rb +36 -0
  70. data/test/helper.rb +176 -32
  71. data/test/internals_test.rb +20 -1
  72. data/test/lint/blocking_commands.rb +40 -16
  73. data/test/lint/hashes.rb +41 -0
  74. data/test/lint/hyper_log_log.rb +15 -1
  75. data/test/lint/lists.rb +16 -0
  76. data/test/lint/sets.rb +142 -0
  77. data/test/lint/sorted_sets.rb +183 -2
  78. data/test/lint/strings.rb +102 -0
  79. data/test/pipelining_commands_test.rb +8 -0
  80. data/test/support/cluster/orchestrator.rb +199 -0
  81. data/test/support/redis_mock.rb +1 -1
  82. data/test/transactions_test.rb +10 -0
  83. metadata +81 -2
@@ -66,4 +66,14 @@ class TestDistributedCommandsOnStrings < Test::Unit::TestCase
66
66
  end
67
67
  end
68
68
  end
69
+
70
+ def test_mapped_mget_in_a_pipeline_returns_hash
71
+ assert_raise Redis::Distributed::CannotDistribute do
72
+ super
73
+ end
74
+ end
75
+
76
+ def test_bitfield
77
+ # Not implemented yet
78
+ end
69
79
  end
@@ -38,6 +38,42 @@ class TestDistributedCommandsOnValueTypes < Test::Unit::TestCase
38
38
  assert_equal [], r.keys("*").sort
39
39
  end
40
40
 
41
+ def test_unlink
42
+ target_version "4.0.0" do
43
+ r.set "foo", "s1"
44
+ r.set "bar", "s2"
45
+ r.set "baz", "s3"
46
+
47
+ assert_equal ["bar", "baz", "foo"], r.keys("*").sort
48
+
49
+ assert_equal 1, r.unlink("foo")
50
+
51
+ assert_equal ["bar", "baz"], r.keys("*").sort
52
+
53
+ assert_equal 2, r.unlink("bar", "baz")
54
+
55
+ assert_equal [], r.keys("*").sort
56
+ end
57
+ end
58
+
59
+ def test_unlink_with_array_argument
60
+ target_version "4.0.0" do
61
+ r.set "foo", "s1"
62
+ r.set "bar", "s2"
63
+ r.set "baz", "s3"
64
+
65
+ assert_equal ["bar", "baz", "foo"], r.keys("*").sort
66
+
67
+ assert_equal 1, r.unlink(["foo"])
68
+
69
+ assert_equal ["bar", "baz"], r.keys("*").sort
70
+
71
+ assert_equal 2, r.unlink(["bar", "baz"])
72
+
73
+ assert_equal [], r.keys("*").sort
74
+ end
75
+ end
76
+
41
77
  def test_randomkey
42
78
  assert_raise Redis::Distributed::CannotDistribute do
43
79
  r.randomkey
@@ -1,4 +1,5 @@
1
1
  require "test/unit"
2
+ require "mocha/test_unit"
2
3
  require "logger"
3
4
  require "stringio"
4
5
 
@@ -12,39 +13,12 @@ require_relative "../lib/redis/connection/#{ENV["DRIVER"]}"
12
13
 
13
14
  require_relative "support/redis_mock"
14
15
  require_relative "support/connection/#{ENV["DRIVER"]}"
16
+ require_relative 'support/cluster/orchestrator'
15
17
 
16
18
  PORT = 6381
17
19
  OPTIONS = {:port => PORT, :db => 15, :timeout => Float(ENV["TIMEOUT"] || 0.1)}
18
20
  NODES = ["redis://127.0.0.1:#{PORT}/15"]
19
21
 
20
- def init(redis)
21
- begin
22
- redis.select 14
23
- redis.flushdb
24
- redis.select 15
25
- redis.flushdb
26
- redis
27
- rescue Redis::CannotConnectError
28
- puts <<-EOS
29
-
30
- Cannot connect to Redis.
31
-
32
- Make sure Redis is running on localhost, port #{PORT}.
33
- This testing suite connects to the database 15.
34
-
35
- Try this once:
36
-
37
- $ make clean
38
-
39
- Then run the build again:
40
-
41
- $ make
42
-
43
- EOS
44
- exit 1
45
- end
46
- end
47
-
48
22
  def driver(*drivers, &blk)
49
23
  if drivers.map(&:to_s).include?(ENV["DRIVER"])
50
24
  class_eval(&blk)
@@ -133,6 +107,32 @@ module Helper
133
107
  @redis.quit if @redis
134
108
  end
135
109
 
110
+ def init(redis)
111
+ redis.select 14
112
+ redis.flushdb
113
+ redis.select 15
114
+ redis.flushdb
115
+ redis
116
+ rescue Redis::CannotConnectError
117
+ puts <<-MSG
118
+
119
+ Cannot connect to Redis.
120
+
121
+ Make sure Redis is running on localhost, port #{PORT}.
122
+ This testing suite connects to the database 15.
123
+
124
+ Try this once:
125
+
126
+ $ make clean
127
+
128
+ Then run the build again:
129
+
130
+ $ make
131
+
132
+ MSG
133
+ exit 1
134
+ end
135
+
136
136
  def redis_mock(commands, options = {}, &blk)
137
137
  RedisMock.start(commands, options) do |port|
138
138
  yield _new_client(options.merge(:port => port))
@@ -156,16 +156,16 @@ module Helper
156
156
  yield
157
157
  end
158
158
  end
159
+
160
+ def version
161
+ Version.new(redis.info['redis_version'])
162
+ end
159
163
  end
160
164
 
161
165
  module Client
162
166
 
163
167
  include Generic
164
168
 
165
- def version
166
- Version.new(redis.info["redis_version"])
167
- end
168
-
169
169
  private
170
170
 
171
171
  def _format_options(options)
@@ -198,4 +198,148 @@ module Helper
198
198
  Redis::Distributed.new(NODES, _format_options(options).merge(:driver => ENV["conn"]))
199
199
  end
200
200
  end
201
+
202
+ module Cluster
203
+ include Generic
204
+
205
+ DEFAULT_HOST = '127.0.0.1'
206
+ DEFAULT_PORTS = (7000..7005).freeze
207
+
208
+ ClusterSlotsRawReply = lambda { |host, port|
209
+ # @see https://redis.io/topics/protocol
210
+ <<-REPLY.delete(' ')
211
+ *1\r
212
+ *4\r
213
+ :0\r
214
+ :16383\r
215
+ *3\r
216
+ $#{host.size}\r
217
+ #{host}\r
218
+ :#{port}\r
219
+ $40\r
220
+ 649fa246273043021a05f547a79478597d3f1dc5\r
221
+ *3\r
222
+ $#{host.size}\r
223
+ #{host}\r
224
+ :#{port}\r
225
+ $40\r
226
+ 649fa246273043021a05f547a79478597d3f1dc5\r
227
+ REPLY
228
+ }
229
+
230
+ ClusterNodesRawReply = lambda { |host, port|
231
+ line = "649fa246273043021a05f547a79478597d3f1dc5 #{host}:#{port}@17000 "\
232
+ 'myself,master - 0 1530797742000 1 connected 0-16383'
233
+ "$#{line.size}\r\n#{line}\r\n"
234
+ }
235
+
236
+ def init(redis)
237
+ redis.flushall
238
+ redis
239
+ rescue Redis::CannotConnectError
240
+ puts <<-MSG
241
+
242
+ Cannot connect to Redis Cluster.
243
+
244
+ Make sure Redis is running on localhost, port #{DEFAULT_PORTS}.
245
+
246
+ Try this once:
247
+
248
+ $ make stop_cluster
249
+
250
+ Then run the build again:
251
+
252
+ $ make
253
+
254
+ MSG
255
+ exit 1
256
+ end
257
+
258
+ def build_another_client(options = {})
259
+ _new_client(options)
260
+ end
261
+
262
+ def redis_cluster_mock(commands, options = {})
263
+ host = DEFAULT_HOST
264
+ port = nil
265
+
266
+ cluster_subcommands = if commands.key?(:cluster)
267
+ commands.delete(:cluster)
268
+ .map { |k, v| [k.to_s.downcase, v] }
269
+ .to_h
270
+ else
271
+ {}
272
+ end
273
+
274
+ commands[:cluster] = lambda { |subcommand, *args|
275
+ if cluster_subcommands.key?(subcommand)
276
+ cluster_subcommands[subcommand].call(*args)
277
+ else
278
+ case subcommand
279
+ when 'slots' then ClusterSlotsRawReply.call(host, port)
280
+ when 'nodes' then ClusterNodesRawReply.call(host, port)
281
+ else '+OK'
282
+ end
283
+ end
284
+ }
285
+
286
+ commands[:command] = ->(*_) { "*0\r\n" }
287
+
288
+ RedisMock.start(commands, options) do |po|
289
+ port = po
290
+ scheme = options[:ssl] ? 'rediss' : 'redis'
291
+ nodes = %W[#{scheme}://#{host}:#{port}]
292
+ yield _new_client(options.merge(cluster: nodes))
293
+ end
294
+ end
295
+
296
+ def redis_cluster_down
297
+ trib = ClusterOrchestrator.new(_default_nodes)
298
+ trib.down
299
+ yield
300
+ ensure
301
+ trib.rebuild
302
+ trib.close
303
+ end
304
+
305
+ def redis_cluster_failover
306
+ trib = ClusterOrchestrator.new(_default_nodes)
307
+ trib.failover
308
+ yield
309
+ ensure
310
+ trib.rebuild
311
+ trib.close
312
+ end
313
+
314
+ # @param slot [Integer]
315
+ # @param src [String] <ip>:<port>
316
+ # @param dest [String] <ip>:<port>
317
+ def redis_cluster_resharding(slot, src:, dest:)
318
+ trib = ClusterOrchestrator.new(_default_nodes)
319
+ trib.start_resharding(slot, src, dest)
320
+ yield
321
+ trib.finish_resharding(slot, dest)
322
+ ensure
323
+ trib.rebuild
324
+ trib.close
325
+ end
326
+
327
+ private
328
+
329
+ def _default_nodes(host: DEFAULT_HOST, ports: DEFAULT_PORTS)
330
+ ports.map { |port| "redis://#{host}:#{port}" }
331
+ end
332
+
333
+ def _format_options(options)
334
+ {
335
+ timeout: OPTIONS[:timeout],
336
+ logger: ::Logger.new(@log),
337
+ cluster: _default_nodes
338
+ }.merge(options)
339
+ end
340
+
341
+ def _new_client(options = {})
342
+ Redis.new(_format_options(options).merge(driver: ENV['DRIVER']))
343
+ end
344
+ end
201
345
  end
@@ -202,6 +202,19 @@ class TestInternals < Test::Unit::TestCase
202
202
  end
203
203
  end
204
204
 
205
+ def test_retry_with_custom_reconnect_attempts_and_exponential_backoff
206
+ close_on_ping([0, 1, 2], :reconnect_attempts => 3,
207
+ :reconnect_delay_max => 0.5,
208
+ :reconnect_delay => 0.01) do |redis|
209
+
210
+ Kernel.expects(:sleep).with(0.01).returns(true)
211
+ Kernel.expects(:sleep).with(0.02).returns(true)
212
+ Kernel.expects(:sleep).with(0.04).returns(true)
213
+
214
+ assert_equal "3", redis.ping
215
+ end
216
+ end
217
+
205
218
  def test_don_t_retry_when_second_read_in_pipeline_raises_econnreset
206
219
  close_on_ping([1]) do |redis|
207
220
  assert_raise Redis::ConnectionError do
@@ -334,7 +347,13 @@ class TestInternals < Test::Unit::TestCase
334
347
  begin
335
348
  sa = Socket.pack_sockaddr_in(1024 + Random.rand(63076), hosts[af])
336
349
  s.bind(sa)
337
- rescue Errno::EADDRINUSE
350
+ rescue Errno::EADDRINUSE => e
351
+ # On JRuby (9.1.15.0), if IPv6 is globally disabled on the system,
352
+ # we get an EADDRINUSE with belows message.
353
+ if e.message =~ /Protocol family unavailable/
354
+ return
355
+ end
356
+
338
357
  tries -= 1
339
358
  retry if tries > 0
340
359
 
@@ -1,14 +1,15 @@
1
1
  module Lint
2
-
3
2
  module BlockingCommands
4
-
5
3
  def setup
6
4
  super
7
5
 
8
- r.rpush("{zap}foo", "s1")
9
- r.rpush("{zap}foo", "s2")
10
- r.rpush("{zap}bar", "s1")
11
- r.rpush("{zap}bar", "s2")
6
+ r.rpush('{zap}foo', 's1')
7
+ r.rpush('{zap}foo', 's2')
8
+ r.rpush('{zap}bar', 's1')
9
+ r.rpush('{zap}bar', 's2')
10
+
11
+ r.zadd('{szap}foo', %w[0 a 1 b 2 c])
12
+ r.zadd('{szap}bar', %w[0 c 1 d 2 e])
12
13
  end
13
14
 
14
15
  def to_protocol(obj)
@@ -18,27 +19,38 @@ module Lint
18
19
  when Array
19
20
  "*#{obj.length}\r\n" + obj.map { |e| to_protocol(e) }.join
20
21
  else
21
- fail
22
+ raise
22
23
  end
23
24
  end
24
25
 
25
26
  def mock(options = {}, &blk)
26
- commands = {
27
- :blpop => lambda do |*args|
28
- sleep options[:delay] if options.has_key?(:delay)
27
+ commands = build_mock_commands(options)
28
+ redis_mock(commands, &blk)
29
+ end
30
+
31
+ def build_mock_commands(options = {})
32
+ {
33
+ blpop: lambda do |*args|
34
+ sleep options[:delay] if options.key?(:delay)
29
35
  to_protocol([args.first, args.last])
30
36
  end,
31
- :brpop => lambda do |*args|
32
- sleep options[:delay] if options.has_key?(:delay)
37
+ brpop: lambda do |*args|
38
+ sleep options[:delay] if options.key?(:delay)
33
39
  to_protocol([args.first, args.last])
34
40
  end,
35
- :brpoplpush => lambda do |*args|
36
- sleep options[:delay] if options.has_key?(:delay)
41
+ brpoplpush: lambda do |*args|
42
+ sleep options[:delay] if options.key?(:delay)
37
43
  to_protocol(args.last)
44
+ end,
45
+ bzpopmax: lambda do |*args|
46
+ sleep options[:delay] if options.key?(:delay)
47
+ to_protocol([args.first, args.last])
48
+ end,
49
+ bzpopmin: lambda do |*args|
50
+ sleep options[:delay] if options.key?(:delay)
51
+ to_protocol([args.first, args.last])
38
52
  end
39
53
  }
40
-
41
- redis_mock(commands, &blk)
42
54
  end
43
55
 
44
56
  def test_blpop
@@ -121,6 +133,18 @@ module Lint
121
133
  end
122
134
  end
123
135
 
136
+ def test_bzpopmin
137
+ target_version('4.9.0') do
138
+ assert_equal %w[{szap}foo a 0], r.bzpopmin('{szap}foo', '{szap}bar', 0)
139
+ end
140
+ end
141
+
142
+ def test_bzpopmax
143
+ target_version('4.9.0') do
144
+ assert_equal %w[{szap}foo c 2], r.bzpopmax('{szap}foo', '{szap}bar', 0)
145
+ end
146
+ end
147
+
124
148
  driver(:ruby, :hiredis) do
125
149
  def test_blpop_socket_timeout
126
150
  mock(:delay => 1 + OPTIONS[:timeout] * 2) do |r|
@@ -30,6 +30,21 @@ module Lint
30
30
  assert_equal nil, r.hget("foo", "f1")
31
31
  end
32
32
 
33
+ def test_splat_hdel
34
+ target_version "2.3.9" do
35
+ r.hset("foo", "f1", "s1")
36
+ r.hset("foo", "f2", "s2")
37
+
38
+ assert_equal "s1", r.hget("foo", "f1")
39
+ assert_equal "s2", r.hget("foo", "f2")
40
+
41
+ assert_equal 2, r.hdel("foo", "f1", "f2")
42
+
43
+ assert_equal nil, r.hget("foo", "f1")
44
+ assert_equal nil, r.hget("foo", "f2")
45
+ end
46
+ end
47
+
33
48
  def test_variadic_hdel
34
49
  target_version "2.3.9" do
35
50
  r.hset("foo", "f1", "s1")
@@ -129,6 +144,17 @@ module Lint
129
144
  assert({"f1" => "s1", "f2" => "s2"} == r.mapped_hmget("foo", "f1", "f2"))
130
145
  end
131
146
 
147
+ def test_mapped_hmget_in_a_pipeline_returns_hash
148
+ r.hset("foo", "f1", "s1")
149
+ r.hset("foo", "f2", "s2")
150
+
151
+ result = r.pipelined do
152
+ r.mapped_hmget("foo", "f1", "f2")
153
+ end
154
+
155
+ assert_equal result[0], { "f1" => "s1", "f2" => "s2" }
156
+ end
157
+
132
158
  def test_hincrby
133
159
  r.hincrby("foo", "f1", 1)
134
160
 
@@ -158,5 +184,20 @@ module Lint
158
184
  assert_equal "1.9", r.hget("foo", "f1")
159
185
  end
160
186
  end
187
+
188
+ def test_hstrlen
189
+ target_version('3.2.0') do
190
+ redis.hmset('foo', 'f1', 'HelloWorld', 'f2', 99, 'f3', -256)
191
+ assert_equal 10, r.hstrlen('foo', 'f1')
192
+ assert_equal 2, r.hstrlen('foo', 'f2')
193
+ assert_equal 4, r.hstrlen('foo', 'f3')
194
+ end
195
+ end
196
+
197
+ def test_hscan
198
+ redis.hmset('foo', 'f1', 'Jack', 'f2', 33)
199
+ expected = ['0', [%w[f1 Jack], %w[f2 33]]]
200
+ assert_equal expected, redis.hscan('foo', 0)
201
+ end
161
202
  end
162
203
  end