redis 4.1.0 → 4.1.2

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
- SHA1:
3
- metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
4
- data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
2
+ SHA256:
3
+ metadata.gz: c0b16163b5714933ba20edb9595d7669c7270502586546f3e8758192039113c1
4
+ data.tar.gz: 150dacb8ae77a51536e3f2bc6813144b0176415d008adaa5d5e6c4d7f25624a1
5
5
  SHA512:
6
- metadata.gz: b39badbb4689a4ea93cbc65ad00f0c967f24dadcc41e7750ab82977176236d6d8609d8e7b74e13a3829ce008c4a5de90930e265f6b6e03af710811419da6ccf6
7
- data.tar.gz: 3ede92146cb181328657a4713e94473f86661dffe0a66d23f48edaa2c7a6c52543a7e7036794baa9dcc2945bc3f9e5eebd9192b70510bda1ad9c0d6e67253830
6
+ metadata.gz: 90a9fcfdefc41f5feaedfea2bde2be1ad39308dee10f0045ec227577d067d795aaaede4aec200ed6f5777167842540ff5adb6a927fe6a0a0e5ac46ea8eeaf737
7
+ data.tar.gz: 90c74c87892ef96d0d6ef2bd5934f8e3064d30064a9310fc2b1e31aba066ccb6a005d030458a35d9ad09ed403749e3356df3072927e6a749fc21caedee75f936
@@ -1,5 +1,22 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.1.2
4
+
5
+ * Fix the client hanging forever when connecting with SSL to a non-SSL server. See #835.
6
+ * Fix several authentication problems with sentinel. See #850 and #856.
7
+ * Explicitly drop Ruby 2.2 support.
8
+
9
+
10
+ # 4.1.1
11
+
12
+ * Fix error handling in multi blocks. See #754.
13
+ * Fix geoadd to accept arrays like georadius and georadiusbymember. See #841.
14
+ * Fix georadius command failing when long == lat. See #841.
15
+ * Fix timeout error in xread block: 0. See #837.
16
+ * Fix incompatibility issue with redis-objects. See #834.
17
+ * Properly handle Errno::EADDRNOTAVAIL on connect.
18
+ * Fix password authentication to sentinel instances. See #813.
19
+
3
20
  # 4.1.0
4
21
 
5
22
  * Add Redis Cluster support. See #716.
data/README.md CHANGED
@@ -95,6 +95,55 @@ 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
+ If you want to [authenticate](https://redis.io/topics/sentinel#configuring-sentinel-instances-with-authentication) Sentinel itself, you must specify the `password` option per instance.
99
+
100
+ ```ruby
101
+ SENTINELS = [{ host: '127.0.0.1', port: 26380, password: 'mysecret' },
102
+ { host: '127.0.0.1', port: 26381, password: 'mysecret' }]
103
+
104
+ redis = Redis.new(host: 'mymaster', sentinels: SENTINELS, role: :master)
105
+ ```
106
+
107
+ ## Cluster support
108
+
109
+ `redis-rb` supports [clustering](https://redis.io/topics/cluster-spec).
110
+
111
+ ```ruby
112
+ # Nodes can be passed to the client as an array of connection URLs.
113
+ nodes = (7000..7005).map { |port| "redis://127.0.0.1:#{port}" }
114
+ redis = Redis.new(cluster: nodes)
115
+
116
+ # You can also specify the options as a Hash. The options are the same as for a single server connection.
117
+ (7000..7005).map { |port| { host: '127.0.0.1', port: port } }
118
+ ```
119
+
120
+ 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.
121
+
122
+ ```ruby
123
+ Redis.new(cluster: %w[redis://127.0.0.1:7000])
124
+ ```
125
+
126
+ 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.
127
+
128
+ ```ruby
129
+ Redis.new(cluster: nodes, replica: true)
130
+ ```
131
+
132
+ The calling code is responsible for [avoiding cross slot commands](https://redis.io/topics/cluster-spec#keys-distribution-model).
133
+
134
+ ```ruby
135
+ redis = Redis.new(cluster: %w[redis://127.0.0.1:7000])
136
+
137
+ redis.mget('key1', 'key2')
138
+ #=> Redis::CommandError (CROSSSLOT Keys in request don't hash to the same slot)
139
+
140
+ redis.mget('{key}1', '{key}2')
141
+ #=> [nil, nil]
142
+ ```
143
+
144
+ * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
145
+ * The client supports `MOVED` and `ASK` redirections transparently.
146
+
98
147
  ## Storing objects
99
148
 
100
149
  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)
@@ -2815,10 +2823,10 @@ class Redis
2815
2823
  #
2816
2824
  # @param [String] key
2817
2825
  # @param [Array] member arguemnts for member or members: longitude, latitude, name
2818
- # @return [Intger] number of elements added to the sorted set
2826
+ # @return [Integer] 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
@@ -568,6 +577,7 @@ class Redis
568
577
  client = Client.new(@options.merge({
569
578
  :host => sentinel[:host],
570
579
  :port => sentinel[:port],
580
+ password: sentinel[:password],
571
581
  :reconnect_attempts => 0,
572
582
  }))
573
583
 
@@ -599,9 +609,19 @@ class Redis
599
609
  def resolve_slave
600
610
  sentinel_detect do |client|
601
611
  if reply = client.call(["sentinel", "slaves", @master])
602
- slave = Hash[*reply.sample]
603
-
604
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
612
+ slaves = reply.map { |s| s.each_slice(2).to_h }
613
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
614
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
615
+
616
+ if slaves.empty?
617
+ raise CannotConnectError, 'No slaves available.'
618
+ else
619
+ slave = slaves.sample
620
+ {
621
+ host: slave.fetch('ip'),
622
+ port: slave.fetch('port'),
623
+ }
624
+ end
605
625
  end
606
626
  end
607
627
  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
@@ -266,7 +266,28 @@ class Redis
266
266
 
267
267
  ssl_sock = new(tcp_sock, ctx)
268
268
  ssl_sock.hostname = host
269
- ssl_sock.connect
269
+
270
+ begin
271
+ # Initiate the socket connection in the background. If it doesn't fail
272
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
273
+ # indicating the connection is in progress.
274
+ # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
275
+ # connections during the connect phase properly, because IO.select only partially works.
276
+ # Instead, you have to retry.
277
+ ssl_sock.connect_nonblock
278
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
279
+ if IO.select([ssl_sock], nil, nil, timeout)
280
+ retry
281
+ else
282
+ raise TimeoutError
283
+ end
284
+ rescue IO::WaitWritable
285
+ if IO.select(nil, [ssl_sock], nil, timeout)
286
+ retry
287
+ else
288
+ raise TimeoutError
289
+ end
290
+ end
270
291
 
271
292
  unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
272
293
  ssl_sock.post_connection_check(host)
@@ -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.2'
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ezra Zygmuntowicz
@@ -16,22 +16,8 @@ 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-30 00:00:00.000000000 Z
20
20
  dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: test-unit
23
- requirement: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- version: 3.1.5
28
- type: :development
29
- prerelease: false
30
- version_requirements: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - ">="
33
- - !ruby/object:Gem::Version
34
- version: 3.1.5
35
21
  - !ruby/object:Gem::Dependency
36
22
  name: mocha
37
23
  requirement: !ruby/object:Gem::Requirement
@@ -122,15 +108,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
108
  requirements:
123
109
  - - ">="
124
110
  - !ruby/object:Gem::Version
125
- version: 2.2.2
111
+ version: 2.3.0
126
112
  required_rubygems_version: !ruby/object:Gem::Requirement
127
113
  requirements:
128
114
  - - ">="
129
115
  - !ruby/object:Gem::Version
130
116
  version: '0'
131
117
  requirements: []
132
- rubyforge_project:
133
- rubygems_version: 2.5.1
118
+ rubygems_version: 3.0.3
134
119
  signing_key:
135
120
  specification_version: 4
136
121
  summary: A Ruby client library for Redis