redis 4.1.0 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
4
- data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
3
+ metadata.gz: fba6921160b00d34cc11548defceaaee4fc9c181
4
+ data.tar.gz: e5eac2ace205355e4890084cc96fe98c651e8d30
5
5
  SHA512:
6
- metadata.gz: b39badbb4689a4ea93cbc65ad00f0c967f24dadcc41e7750ab82977176236d6d8609d8e7b74e13a3829ce008c4a5de90930e265f6b6e03af710811419da6ccf6
7
- data.tar.gz: 3ede92146cb181328657a4713e94473f86661dffe0a66d23f48edaa2c7a6c52543a7e7036794baa9dcc2945bc3f9e5eebd9192b70510bda1ad9c0d6e67253830
6
+ metadata.gz: 5e88bba869f876bc046935479283f108b46107ab00e7ab60487ca524977635dbdc9b0dcfd037a9b057e43765975c26cdc3a0ceea95aefacf608041a18b50f14e
7
+ data.tar.gz: 91360e891b269c8ecb962414f29d023ace19af98e929d7f7680590dcb8520a87deb6c2a47ba27da0ea16eb47ee90d4c8bdf2be894a40c00820f9c07479188cc2
data/README.md CHANGED
@@ -95,6 +95,46 @@ but a few so that if one is down the client will try the next one. The client
95
95
  is able to remember the last Sentinel that was able to reply correctly and will
96
96
  use it for the next requests.
97
97
 
98
+ ## Cluster support
99
+
100
+ `redis-rb` supports [clustering](https://redis.io/topics/cluster-spec).
101
+
102
+ ```ruby
103
+ # Nodes can be passed to the client as an array of connection URLs.
104
+ nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }
105
+ redis = Redis.new(cluster: nodes)
106
+
107
+ # You can also specify the options as a Hash. The options are the same as for a single server connection.
108
+ (7000..7005).map { |port| { host: '127.0.0.1', port: port } }
109
+ ```
110
+
111
+ You can also specify only a subset of the nodes, and the client will discover the missing ones using the [CLUSTER NODES](https://redis.io/commands/cluster-nodes) command.
112
+
113
+ ```ruby
114
+ Redis.new(cluster: %w[redis://127.0.0.1:7000])
115
+ ```
116
+
117
+ If you want [the connection to be able to read from any replica](https://redis.io/commands/readonly), you must pass the `replica: true`. Note that this connection won't be usable to write keys.
118
+
119
+ ```ruby
120
+ Redis.new(cluster: nodes, replica: true)
121
+ ```
122
+
123
+ The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model).
124
+
125
+ ```ruby
126
+ redis = Redis.new(cluster: %w[redis://127.0.0.1:7000])
127
+
128
+ redis.mget('key1', 'key2')
129
+ #=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot)
130
+
131
+ redis.mget('{key}1', '{key}2')
132
+ #=> [nil, nil]
133
+ ```
134
+
135
+ * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
136
+ * The client supports `MOVED` and `ASK` redirections transparently.
137
+
98
138
  ## Storing objects
99
139
 
100
140
  Redis only stores strings as values. If you want to store an object, you
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
4
  require_relative "redis/errors"
3
5
 
@@ -104,7 +106,12 @@ class Redis
104
106
  def commit
105
107
  synchronize do |client|
106
108
  begin
107
- client.call_pipelined(@queue[Thread.current.object_id])
109
+ pipeline = Pipeline.new(client)
110
+ @queue[Thread.current.object_id].each do |command|
111
+ pipeline.call(command)
112
+ end
113
+
114
+ client.call_pipelined(pipeline)
108
115
  ensure
109
116
  @queue.delete(Thread.current.object_id)
110
117
  end
@@ -2403,7 +2410,8 @@ class Redis
2403
2410
  def pipelined
2404
2411
  synchronize do |client|
2405
2412
  begin
2406
- original, @client = @client, Pipeline.new
2413
+ pipeline = Pipeline.new(@client)
2414
+ original, @client = @client, pipeline
2407
2415
  yield(self)
2408
2416
  original.call_pipeline(@client)
2409
2417
  ensure
@@ -2448,7 +2456,7 @@ class Redis
2448
2456
  client.call([:multi])
2449
2457
  else
2450
2458
  begin
2451
- pipeline = Pipeline::Multi.new
2459
+ pipeline = Pipeline::Multi.new(@client)
2452
2460
  original, @client = @client, pipeline
2453
2461
  yield(self)
2454
2462
  original.call_pipeline(pipeline)
@@ -2818,7 +2826,7 @@ class Redis
2818
2826
  # @return [Intger] number of elements added to the sorted set
2819
2827
  def geoadd(key, *member)
2820
2828
  synchronize do |client|
2821
- client.call([:geoadd, key, member])
2829
+ client.call([:geoadd, key, *member])
2822
2830
  end
2823
2831
  end
2824
2832
 
@@ -2977,24 +2985,6 @@ class Redis
2977
2985
  synchronize { |client| client.call(args) }
2978
2986
  end
2979
2987
 
2980
- # Fetches entries of the stream.
2981
- #
2982
- # @example Without options
2983
- # redis.xrange('mystream')
2984
- # @example With first entry id option
2985
- # redis.xrange('mystream', first: '0-1')
2986
- # @example With first and last entry id options
2987
- # redis.xrange('mystream', first: '0-1', last: '0-3')
2988
- # @example With count options
2989
- # redis.xrange('mystream', count: 10)
2990
- #
2991
- # @param key [String] the stream key
2992
- # @param start [String] first entry id of range, default value is `+`
2993
- # @param end [String] last entry id of range, default value is `-`
2994
- # @param count [Integer] the number of entries as limit
2995
- #
2996
- # @return [Hash{String => Hash}] the entries
2997
-
2998
2988
  # Fetches entries of the stream in ascending order.
2999
2989
  #
3000
2990
  # @example Without options
@@ -3327,131 +3317,135 @@ private
3327
3317
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
3328
3318
  # where the method call will return nil. Propagate the nil instead of falsely
3329
3319
  # returning false.
3330
- Boolify =
3331
- lambda { |value|
3332
- value == 1 if value
3333
- }
3320
+ Boolify = lambda { |value|
3321
+ case value
3322
+ when 1
3323
+ true
3324
+ when 0
3325
+ false
3326
+ else
3327
+ value
3328
+ end
3329
+ }
3334
3330
 
3335
- BoolifySet =
3336
- lambda { |value|
3337
- if value && "OK" == value
3338
- true
3339
- else
3340
- false
3341
- end
3342
- }
3331
+ BoolifySet = lambda { |value|
3332
+ case value
3333
+ when "OK"
3334
+ true
3335
+ when nil
3336
+ false
3337
+ else
3338
+ value
3339
+ end
3340
+ }
3343
3341
 
3344
- Hashify =
3345
- lambda { |array|
3346
- hash = Hash.new
3347
- array.each_slice(2) do |field, value|
3348
- hash[field] = value
3349
- end
3350
- hash
3351
- }
3342
+ Hashify = lambda { |value|
3343
+ if value.respond_to?(:each_slice)
3344
+ value.each_slice(2).to_h
3345
+ else
3346
+ value
3347
+ end
3348
+ }
3349
+
3350
+ Floatify = lambda { |value|
3351
+ case value
3352
+ when "inf"
3353
+ Float::INFINITY
3354
+ when "-inf"
3355
+ -Float::INFINITY
3356
+ when String
3357
+ Float(value)
3358
+ else
3359
+ value
3360
+ end
3361
+ }
3352
3362
 
3353
- Floatify =
3354
- lambda { |str|
3355
- if str
3356
- if (inf = str.match(/^(-)?inf/i))
3357
- (inf[1] ? -1.0 : 1.0) / 0.0
3358
- else
3359
- Float(str)
3360
- end
3361
- end
3362
- }
3363
+ FloatifyPairs = lambda { |value|
3364
+ return value unless value.respond_to?(:each_slice)
3363
3365
 
3364
- FloatifyPairs =
3365
- lambda { |result|
3366
- result.each_slice(2).map do |member, score|
3367
- [member, Floatify.call(score)]
3368
- end
3369
- }
3366
+ value.each_slice(2).map do |member, score|
3367
+ [member, Floatify.call(score)]
3368
+ end
3369
+ }
3370
3370
 
3371
- HashifyInfo =
3372
- lambda { |reply|
3373
- Hash[reply.split("\r\n").map do |line|
3374
- line.split(':', 2) unless line =~ /^(#|$)/
3375
- end.compact]
3376
- }
3371
+ HashifyInfo = lambda { |reply|
3372
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
3373
+ lines.map! { |line| line.split(':', 2) }
3374
+ lines.compact!
3375
+ lines.to_h
3376
+ }
3377
3377
 
3378
- HashifyStreams =
3379
- lambda { |reply|
3380
- return {} if reply.nil?
3381
- reply.map do |stream_key, entries|
3382
- [stream_key, HashifyStreamEntries.call(entries)]
3383
- end.to_h
3384
- }
3378
+ HashifyStreams = lambda { |reply|
3379
+ case reply
3380
+ when nil
3381
+ {}
3382
+ else
3383
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
3384
+ end
3385
+ }
3385
3386
 
3386
- HashifyStreamEntries =
3387
- lambda { |reply|
3388
- reply.map do |entry_id, values|
3389
- [entry_id, values.each_slice(2).to_h]
3390
- end
3387
+ HashifyStreamEntries = lambda { |reply|
3388
+ reply.map do |entry_id, values|
3389
+ [entry_id, values.each_slice(2).to_h]
3390
+ end
3391
+ }
3392
+
3393
+ HashifyStreamPendings = lambda { |reply|
3394
+ {
3395
+ 'size' => reply[0],
3396
+ 'min_entry_id' => reply[1],
3397
+ 'max_entry_id' => reply[2],
3398
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
3391
3399
  }
3400
+ }
3392
3401
 
3393
- HashifyStreamPendings =
3394
- lambda { |reply|
3402
+ HashifyStreamPendingDetails = lambda { |reply|
3403
+ reply.map do |arr|
3395
3404
  {
3396
- 'size' => reply[0],
3397
- 'min_entry_id' => reply[1],
3398
- 'max_entry_id' => reply[2],
3399
- 'consumers' => reply[3].nil? ? {} : Hash[reply[3]]
3405
+ 'entry_id' => arr[0],
3406
+ 'consumer' => arr[1],
3407
+ 'elapsed' => arr[2],
3408
+ 'count' => arr[3]
3400
3409
  }
3401
- }
3410
+ end
3411
+ }
3402
3412
 
3403
- HashifyStreamPendingDetails =
3404
- lambda { |reply|
3405
- reply.map do |arr|
3406
- {
3407
- 'entry_id' => arr[0],
3408
- 'consumer' => arr[1],
3409
- 'elapsed' => arr[2],
3410
- 'count' => arr[3]
3411
- }
3412
- end
3413
+ HashifyClusterNodeInfo = lambda { |str|
3414
+ arr = str.split(' ')
3415
+ {
3416
+ 'node_id' => arr[0],
3417
+ 'ip_port' => arr[1],
3418
+ 'flags' => arr[2].split(','),
3419
+ 'master_node_id' => arr[3],
3420
+ 'ping_sent' => arr[4],
3421
+ 'pong_recv' => arr[5],
3422
+ 'config_epoch' => arr[6],
3423
+ 'link_state' => arr[7],
3424
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3413
3425
  }
3426
+ }
3414
3427
 
3415
- HashifyClusterNodeInfo =
3416
- lambda { |str|
3417
- arr = str.split(' ')
3428
+ HashifyClusterSlots = lambda { |reply|
3429
+ reply.map do |arr|
3430
+ first_slot, last_slot = arr[0..1]
3431
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3432
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3418
3433
  {
3419
- 'node_id' => arr[0],
3420
- 'ip_port' => arr[1],
3421
- 'flags' => arr[2].split(','),
3422
- 'master_node_id' => arr[3],
3423
- 'ping_sent' => arr[4],
3424
- 'pong_recv' => arr[5],
3425
- 'config_epoch' => arr[6],
3426
- 'link_state' => arr[7],
3427
- 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3434
+ 'start_slot' => first_slot,
3435
+ 'end_slot' => last_slot,
3436
+ 'master' => master,
3437
+ 'replicas' => replicas
3428
3438
  }
3429
- }
3430
-
3431
- HashifyClusterSlots =
3432
- lambda { |reply|
3433
- reply.map do |arr|
3434
- first_slot, last_slot = arr[0..1]
3435
- master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3436
- replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3437
- {
3438
- 'start_slot' => first_slot,
3439
- 'end_slot' => last_slot,
3440
- 'master' => master,
3441
- 'replicas' => replicas
3442
- }
3443
- end
3444
- }
3439
+ end
3440
+ }
3445
3441
 
3446
- HashifyClusterNodes =
3447
- lambda { |reply|
3448
- reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3449
- }
3442
+ HashifyClusterNodes = lambda { |reply|
3443
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3444
+ }
3450
3445
 
3451
- HashifyClusterSlaves =
3452
- lambda { |reply|
3453
- reply.map { |str| HashifyClusterNodeInfo.call(str) }
3454
- }
3446
+ HashifyClusterSlaves = lambda { |reply|
3447
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3448
+ }
3455
3449
 
3456
3450
  Noop = ->(reply) { reply }
3457
3451
 
@@ -3459,8 +3453,7 @@ private
3459
3453
  args.push sort if sort
3460
3454
  args.push 'count', count if count
3461
3455
  args.push options if options
3462
-
3463
- args.uniq
3456
+ args
3464
3457
  end
3465
3458
 
3466
3459
  def _subscription(method, timeout, channels, block)
@@ -3488,6 +3481,8 @@ private
3488
3481
  synchronize do |client|
3489
3482
  if blocking_timeout_msec.nil?
3490
3483
  client.call(args, &HashifyStreams)
3484
+ elsif blocking_timeout_msec.to_f.zero?
3485
+ client.call_without_timeout(args, &HashifyStreams)
3491
3486
  else
3492
3487
  timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3493
3488
  client.call_with_timeout(args, timeout, &HashifyStreams)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "errors"
2
4
  require "socket"
3
5
  require "cgi"
@@ -155,12 +157,11 @@ class Redis
155
157
  end
156
158
 
157
159
  def call_pipeline(pipeline)
158
- commands = pipeline.commands
159
- return [] if commands.empty?
160
+ return [] if pipeline.futures.empty?
160
161
 
161
162
  with_reconnect pipeline.with_reconnect? do
162
163
  begin
163
- pipeline.finish(call_pipelined(commands)).tap do
164
+ pipeline.finish(call_pipelined(pipeline)).tap do
164
165
  self.db = pipeline.db if pipeline.db
165
166
  end
166
167
  rescue ConnectionError => e
@@ -173,8 +174,8 @@ class Redis
173
174
  end
174
175
  end
175
176
 
176
- def call_pipelined(commands)
177
- return [] if commands.empty?
177
+ def call_pipelined(pipeline)
178
+ return [] if pipeline.futures.empty?
178
179
 
179
180
  # The method #ensure_connected (called from #process) reconnects once on
180
181
  # I/O errors. To make an effort in making sure that commands are not
@@ -184,6 +185,8 @@ class Redis
184
185
  # already successfully executed commands. To circumvent this, don't retry
185
186
  # after the first reply has been read successfully.
186
187
 
188
+ commands = pipeline.commands
189
+
187
190
  result = Array.new(commands.size)
188
191
  reconnect = @reconnect
189
192
 
@@ -191,8 +194,12 @@ class Redis
191
194
  exception = nil
192
195
 
193
196
  process(commands) do
194
- commands.size.times do |i|
195
- reply = read
197
+ pipeline.timeouts.each_with_index do |timeout, i|
198
+ reply = if timeout
199
+ with_socket_timeout(timeout) { read }
200
+ else
201
+ read
202
+ end
196
203
  result[i] = reply
197
204
  @reconnect = false
198
205
  exception = reply if exception.nil? && reply.is_a?(CommandError)
@@ -343,12 +350,14 @@ class Redis
343
350
  @pending_reads = 0
344
351
  rescue TimeoutError,
345
352
  SocketError,
353
+ Errno::EADDRNOTAVAIL,
346
354
  Errno::ECONNREFUSED,
347
355
  Errno::EHOSTDOWN,
348
356
  Errno::EHOSTUNREACH,
349
357
  Errno::ENETUNREACH,
350
358
  Errno::ENOENT,
351
- Errno::ETIMEDOUT
359
+ Errno::ETIMEDOUT,
360
+ Errno::EINVAL
352
361
 
353
362
  raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
354
363
  end
@@ -407,7 +416,8 @@ class Redis
407
416
  options[key] = options[key.to_s] if options.has_key?(key.to_s)
408
417
  end
409
418
 
410
- url = options[:url] || defaults[:url]
419
+ url = options[:url]
420
+ url = defaults[:url] if url == nil
411
421
 
412
422
  # Override defaults from URL if given
413
423
  if url
@@ -525,7 +535,6 @@ class Redis
525
535
  def initialize(options)
526
536
  super(options)
527
537
 
528
- @options[:password] = DEFAULTS.fetch(:password)
529
538
  @options[:db] = DEFAULTS.fetch(:db)
530
539
 
531
540
  @sentinels = @options.delete(:sentinels).dup
@@ -586,6 +595,11 @@ class Redis
586
595
  end
587
596
 
588
597
  raise CannotConnectError, "No sentinels available."
598
+ rescue Redis::CommandError => err
599
+ # this feature is only available starting with Redis 5.0.1
600
+ raise unless err.message.start_with?('ERR unknown command `auth`')
601
+ @options[:password] = DEFAULTS.fetch(:password)
602
+ retry
589
603
  end
590
604
 
591
605
  def resolve_master
@@ -599,9 +613,19 @@ class Redis
599
613
  def resolve_slave
600
614
  sentinel_detect do |client|
601
615
  if reply = client.call(["sentinel", "slaves", @master])
602
- slave = Hash[*reply.sample]
603
-
604
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
616
+ slaves = reply.map { |s| s.each_slice(2).to_h }
617
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
618
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
619
+
620
+ if slaves.empty?
621
+ raise CannotConnectError, 'No slaves available.'
622
+ else
623
+ slave = slaves.sample
624
+ {
625
+ host: slave.fetch('ip'),
626
+ port: slave.fetch('port'),
627
+ }
628
+ end
605
629
  end
606
630
  end
607
631
  end
@@ -57,7 +57,7 @@ class Redis
57
57
  end
58
58
 
59
59
  def assign_node_key(mappings, slot, node_key)
60
- mappings[slot] ||= { master: nil, slaves: Set.new }
60
+ mappings[slot] ||= { master: nil, slaves: ::Set.new }
61
61
  if master?(node_key)
62
62
  mappings[slot][:master] = node_key
63
63
  else
@@ -1,15 +1,21 @@
1
1
  class Redis
2
2
  class Pipeline
3
3
  attr_accessor :db
4
+ attr_reader :client
4
5
 
5
6
  attr :futures
6
7
 
7
- def initialize
8
+ def initialize(client)
9
+ @client = client.is_a?(Pipeline) ? client.client : client
8
10
  @with_reconnect = true
9
11
  @shutdown = false
10
12
  @futures = []
11
13
  end
12
14
 
15
+ def timeout
16
+ client.timeout
17
+ end
18
+
13
19
  def with_reconnect?
14
20
  @with_reconnect
15
21
  end
@@ -26,15 +32,19 @@ class Redis
26
32
  @futures.empty?
27
33
  end
28
34
 
29
- def call(command, &block)
35
+ def call(command, timeout: nil, &block)
30
36
  # A pipeline that contains a shutdown should not raise ECONNRESET when
31
37
  # the connection is gone.
32
38
  @shutdown = true if command.first == :shutdown
33
- future = Future.new(command, block)
39
+ future = Future.new(command, block, timeout)
34
40
  @futures << future
35
41
  future
36
42
  end
37
43
 
44
+ def call_with_timeout(command, timeout, &block)
45
+ call(command, timeout: timeout, &block)
46
+ end
47
+
38
48
  def call_pipeline(pipeline)
39
49
  @shutdown = true if pipeline.shutdown?
40
50
  @futures.concat(pipeline.futures)
@@ -43,7 +53,11 @@ class Redis
43
53
  end
44
54
 
45
55
  def commands
46
- @futures.map { |f| f._command }
56
+ @futures.map(&:_command)
57
+ end
58
+
59
+ def timeouts
60
+ @futures.map(&:timeout)
47
61
  end
48
62
 
49
63
  def with_reconnect(val=true)
@@ -89,6 +103,14 @@ class Redis
89
103
  end
90
104
  end
91
105
 
106
+ def timeouts
107
+ if empty?
108
+ []
109
+ else
110
+ [nil, *super, nil]
111
+ end
112
+ end
113
+
92
114
  def commands
93
115
  if empty?
94
116
  []
@@ -108,9 +130,12 @@ class Redis
108
130
  class Future < BasicObject
109
131
  FutureNotReady = ::Redis::FutureNotReady.new
110
132
 
111
- def initialize(command, transformation)
133
+ attr_reader :timeout
134
+
135
+ def initialize(command, transformation, timeout)
112
136
  @command = command
113
137
  @transformation = transformation
138
+ @timeout = timeout
114
139
  @object = FutureNotReady
115
140
  end
116
141
 
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = '4.1.0'
2
+ VERSION = '4.1.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ezra Zygmuntowicz
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2018-12-13 00:00:00.000000000 Z
19
+ date: 2019-05-06 00:00:00.000000000 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: test-unit