redis 4.0.1 → 4.0.3

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.
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