redis 4.8.1 → 5.4.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +125 -162
  4. data/lib/redis/client.rb +82 -616
  5. data/lib/redis/commands/bitmaps.rb +14 -4
  6. data/lib/redis/commands/cluster.rb +1 -18
  7. data/lib/redis/commands/connection.rb +5 -10
  8. data/lib/redis/commands/geo.rb +3 -3
  9. data/lib/redis/commands/hashes.rb +13 -6
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +27 -23
  12. data/lib/redis/commands/lists.rb +74 -25
  13. data/lib/redis/commands/pubsub.rb +34 -25
  14. data/lib/redis/commands/server.rb +15 -15
  15. data/lib/redis/commands/sets.rb +35 -40
  16. data/lib/redis/commands/sorted_sets.rb +128 -18
  17. data/lib/redis/commands/streams.rb +48 -21
  18. data/lib/redis/commands/strings.rb +18 -17
  19. data/lib/redis/commands/transactions.rb +7 -31
  20. data/lib/redis/commands.rb +11 -12
  21. data/lib/redis/distributed.rb +136 -72
  22. data/lib/redis/errors.rb +15 -50
  23. data/lib/redis/hash_ring.rb +26 -26
  24. data/lib/redis/pipeline.rb +47 -222
  25. data/lib/redis/subscribe.rb +50 -14
  26. data/lib/redis/version.rb +1 -1
  27. data/lib/redis.rb +77 -184
  28. metadata +10 -57
  29. data/lib/redis/cluster/command.rb +0 -79
  30. data/lib/redis/cluster/command_loader.rb +0 -33
  31. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  32. data/lib/redis/cluster/node.rb +0 -120
  33. data/lib/redis/cluster/node_key.rb +0 -31
  34. data/lib/redis/cluster/node_loader.rb +0 -34
  35. data/lib/redis/cluster/option.rb +0 -100
  36. data/lib/redis/cluster/slot.rb +0 -86
  37. data/lib/redis/cluster/slot_loader.rb +0 -46
  38. data/lib/redis/cluster.rb +0 -315
  39. data/lib/redis/connection/command_helper.rb +0 -41
  40. data/lib/redis/connection/hiredis.rb +0 -68
  41. data/lib/redis/connection/registry.rb +0 -13
  42. data/lib/redis/connection/ruby.rb +0 -437
  43. data/lib/redis/connection/synchrony.rb +0 -148
  44. data/lib/redis/connection.rb +0 -11
data/lib/redis/errors.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  class Redis
4
4
  # Base error for all redis-rb errors.
5
- class BaseError < RuntimeError
5
+ class BaseError < StandardError
6
6
  end
7
7
 
8
8
  # Raised by the connection when a protocol error occurs.
@@ -20,6 +20,15 @@ class Redis
20
20
  class CommandError < BaseError
21
21
  end
22
22
 
23
+ class PermissionError < CommandError
24
+ end
25
+
26
+ class WrongTypeError < CommandError
27
+ end
28
+
29
+ class OutOfMemoryError < CommandError
30
+ end
31
+
23
32
  # Base error for connection related errors.
24
33
  class BaseConnectionError < BaseError
25
34
  end
@@ -40,58 +49,14 @@ class Redis
40
49
  class InheritedError < BaseConnectionError
41
50
  end
42
51
 
52
+ # Generally raised during Redis failover scenarios
53
+ class ReadOnlyError < BaseConnectionError
54
+ end
55
+
43
56
  # Raised when client options are invalid.
44
57
  class InvalidClientOptionError < BaseError
45
58
  end
46
59
 
47
- class Cluster
48
- # Raised when client connected to redis as cluster mode
49
- # and failed to fetch cluster state information by commands.
50
- class InitialSetupError < BaseError
51
- # @param errors [Array<Redis::BaseError>]
52
- def initialize(errors)
53
- super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}")
54
- end
55
- end
56
-
57
- # Raised when client connected to redis as cluster mode
58
- # and some cluster subcommands were called.
59
- class OrchestrationCommandNotSupported < BaseError
60
- def initialize(command, subcommand = '')
61
- str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
62
- msg = "#{str} command should be used with care "\
63
- 'only by applications orchestrating Redis Cluster, like redis-trib, '\
64
- 'and the command if used out of the right context can leave the cluster '\
65
- 'in a wrong state or cause data loss.'
66
- super(msg)
67
- end
68
- end
69
-
70
- # Raised when error occurs on any node of cluster.
71
- class CommandErrorCollection < BaseError
72
- attr_reader :errors
73
-
74
- # @param errors [Hash{String => Redis::CommandError}]
75
- # @param error_message [String]
76
- def initialize(errors, error_message = 'Command errors were replied on any node')
77
- @errors = errors
78
- super(error_message)
79
- end
80
- end
81
-
82
- # Raised when cluster client can't select node.
83
- class AmbiguousNodeError < BaseError
84
- def initialize(command)
85
- super("Cluster client doesn't know which node the #{command} command should be sent to.")
86
- end
87
- end
88
-
89
- # Raised when commands in pipelining include cross slot keys.
90
- class CrossSlotPipeliningError < BaseError
91
- def initialize(keys)
92
- super("Cluster client couldn't send pipelining to single node. "\
93
- "The commands include cross slot keys. #{keys}")
94
- end
95
- end
60
+ class SubscriptionError < BaseError
96
61
  end
97
62
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'zlib'
4
+ require 'digest/md5'
4
5
 
5
6
  class Redis
6
7
  class HashRing
@@ -25,7 +26,7 @@ class Redis
25
26
  def add_node(node)
26
27
  @nodes << node
27
28
  @replicas.times do |i|
28
- key = Zlib.crc32("#{node.id}:#{i}")
29
+ key = server_hash_for("#{node.id}:#{i}")
29
30
  @ring[key] = node
30
31
  @sorted_keys << key
31
32
  end
@@ -35,7 +36,7 @@ class Redis
35
36
  def remove_node(node)
36
37
  @nodes.reject! { |n| n.id == node.id }
37
38
  @replicas.times do |i|
38
- key = Zlib.crc32("#{node.id}:#{i}")
39
+ key = server_hash_for("#{node.id}:#{i}")
39
40
  @ring.delete(key)
40
41
  @sorted_keys.reject! { |k| k == key }
41
42
  end
@@ -43,47 +44,46 @@ class Redis
43
44
 
44
45
  # get the node in the hash ring for this key
45
46
  def get_node(key)
46
- get_node_pos(key)[0]
47
- end
48
-
49
- def get_node_pos(key)
50
- return [nil, nil] if @ring.empty?
51
-
52
- crc = Zlib.crc32(key)
53
- idx = HashRing.binary_search(@sorted_keys, crc)
54
- [@ring[@sorted_keys[idx]], idx]
47
+ hash = hash_for(key)
48
+ idx = binary_search(@sorted_keys, hash)
49
+ @ring[@sorted_keys[idx]]
55
50
  end
56
51
 
57
52
  def iter_nodes(key)
58
53
  return [nil, nil] if @ring.empty?
59
54
 
60
- _, pos = get_node_pos(key)
55
+ crc = hash_for(key)
56
+ pos = binary_search(@sorted_keys, crc)
61
57
  @ring.size.times do |n|
62
58
  yield @ring[@sorted_keys[(pos + n) % @ring.size]]
63
59
  end
64
60
  end
65
61
 
62
+ private
63
+
64
+ def hash_for(key)
65
+ Zlib.crc32(key)
66
+ end
67
+
68
+ def server_hash_for(key)
69
+ Digest::MD5.digest(key).unpack1("L>")
70
+ end
71
+
66
72
  # Find the closest index in HashRing with value <= the given value
67
- def self.binary_search(ary, value)
68
- upper = ary.size - 1
73
+ def binary_search(ary, value)
74
+ upper = ary.size
69
75
  lower = 0
70
- idx = 0
71
-
72
- while lower <= upper
73
- idx = (lower + upper) / 2
74
- comp = ary[idx] <=> value
75
76
 
76
- if comp == 0
77
- return idx
78
- elsif comp > 0
79
- upper = idx - 1
77
+ while lower < upper
78
+ mid = (lower + upper) / 2
79
+ if ary[mid] > value
80
+ upper = mid
80
81
  else
81
- lower = idx + 1
82
+ lower = mid + 1
82
83
  end
83
84
  end
84
85
 
85
- upper = ary.size - 1 if upper < 0
86
- upper
86
+ upper - 1
87
87
  end
88
88
  end
89
89
  end
@@ -4,27 +4,31 @@ require "delegate"
4
4
 
5
5
  class Redis
6
6
  class PipelinedConnection
7
- def initialize(pipeline)
7
+ attr_accessor :db
8
+
9
+ def initialize(pipeline, futures = [], exception: true)
8
10
  @pipeline = pipeline
11
+ @futures = futures
12
+ @exception = exception
9
13
  end
10
14
 
11
15
  include Commands
12
16
 
13
- def db
14
- @pipeline.db
15
- end
16
-
17
- def db=(db)
18
- @pipeline.db = db
19
- end
20
-
21
17
  def pipelined
22
18
  yield self
23
19
  end
24
20
 
25
- def call_pipeline(pipeline)
26
- @pipeline.call_pipeline(pipeline)
27
- nil
21
+ def multi
22
+ transaction = MultiConnection.new(@pipeline, @futures)
23
+ send_command([:multi])
24
+ size = @futures.size
25
+ yield transaction
26
+ multi_future = MultiFuture.new(@futures[size..-1])
27
+ @pipeline.call_v([:exec]) do |result|
28
+ multi_future._set(result)
29
+ end
30
+ @futures << multi_future
31
+ multi_future
28
32
  end
29
33
 
30
34
  private
@@ -34,204 +38,36 @@ class Redis
34
38
  end
35
39
 
36
40
  def send_command(command, &block)
37
- @pipeline.call(command, &block)
38
- end
39
-
40
- def send_blocking_command(command, timeout, &block)
41
- @pipeline.call_with_timeout(command, timeout, &block)
42
- end
43
- end
44
-
45
- class Pipeline
46
- REDIS_INTERNAL_PATH = File.expand_path("..", __dir__).freeze
47
- # Redis use MonitorMixin#synchronize and this class use DelegateClass which we want to filter out.
48
- # Both are in the stdlib so we can simply filter the entire stdlib out.
49
- STDLIB_PATH = File.expand_path("..", MonitorMixin.instance_method(:synchronize).source_location.first).freeze
50
-
51
- class << self
52
- def deprecation_warning(method, caller_locations) # :nodoc:
53
- callsite = caller_locations.find { |l| !l.path.start_with?(REDIS_INTERNAL_PATH, STDLIB_PATH) }
54
- callsite ||= caller_locations.last # The caller_locations should be large enough, but just in case.
55
- ::Redis.deprecate! <<~MESSAGE
56
- Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
57
-
58
- redis.#{method} do
59
- redis.get("key")
60
- end
61
-
62
- should be replaced by
63
-
64
- redis.#{method} do |pipeline|
65
- pipeline.get("key")
66
- end
67
-
68
- (called from #{callsite}}
69
- MESSAGE
41
+ future = Future.new(command, block, @exception)
42
+ @pipeline.call_v(command) do |result|
43
+ future._set(result)
70
44
  end
71
- end
72
-
73
- attr_accessor :db
74
- attr_reader :client
75
-
76
- attr :futures
77
- alias materialized_futures futures
78
-
79
- def initialize(client)
80
- @client = client.is_a?(Pipeline) ? client.client : client
81
- @with_reconnect = true
82
- @shutdown = false
83
- @futures = []
84
- end
85
-
86
- def timeout
87
- client.timeout
88
- end
89
-
90
- def with_reconnect?
91
- @with_reconnect
92
- end
93
-
94
- def without_reconnect?
95
- !@with_reconnect
96
- end
97
-
98
- def shutdown?
99
- @shutdown
100
- end
101
-
102
- def empty?
103
- @futures.empty?
104
- end
105
-
106
- def call(command, timeout: nil, &block)
107
- # A pipeline that contains a shutdown should not raise ECONNRESET when
108
- # the connection is gone.
109
- @shutdown = true if command.first == :shutdown
110
- future = Future.new(command, block, timeout)
111
45
  @futures << future
112
46
  future
113
47
  end
114
48
 
115
- def call_with_timeout(command, timeout, &block)
116
- call(command, timeout: timeout, &block)
117
- end
118
-
119
- def call_pipeline(pipeline)
120
- @shutdown = true if pipeline.shutdown?
121
- @futures.concat(pipeline.materialized_futures)
122
- @db = pipeline.db
123
- nil
124
- end
125
-
126
- def commands
127
- @futures.map(&:_command)
128
- end
129
-
130
- def timeouts
131
- @futures.map(&:timeout)
132
- end
133
-
134
- def with_reconnect(val = true)
135
- @with_reconnect = false unless val
136
- yield
137
- end
138
-
139
- def without_reconnect(&blk)
140
- with_reconnect(false, &blk)
141
- end
142
-
143
- def finish(replies, &blk)
144
- if blk
145
- futures.each_with_index.map do |future, i|
146
- future._set(blk.call(replies[i]))
147
- end
148
- else
149
- futures.each_with_index.map do |future, i|
150
- future._set(replies[i])
151
- end
152
- end
153
- end
154
-
155
- class Multi < self
156
- def finish(replies)
157
- exec = replies.last
158
-
159
- return if exec.nil? # The transaction failed because of WATCH.
160
-
161
- # EXEC command failed.
162
- raise exec if exec.is_a?(CommandError)
163
-
164
- if exec.size < futures.size
165
- # Some command wasn't recognized by Redis.
166
- command_error = replies.detect { |r| r.is_a?(CommandError) }
167
- raise command_error
168
- end
169
-
170
- super(exec) do |reply|
171
- # Because an EXEC returns nested replies, hiredis won't be able to
172
- # convert an error reply to a CommandError instance itself. This is
173
- # specific to MULTI/EXEC, so we solve this here.
174
- reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
175
- end
176
- end
177
-
178
- def materialized_futures
179
- if empty?
180
- []
181
- else
182
- [
183
- Future.new([:multi], nil, 0),
184
- *futures,
185
- MultiFuture.new(futures)
186
- ]
187
- end
188
- end
189
-
190
- def timeouts
191
- if empty?
192
- []
193
- else
194
- [nil, *super, nil]
195
- end
196
- end
197
-
198
- def commands
199
- if empty?
200
- []
201
- else
202
- [[:multi]] + super + [[:exec]]
203
- end
49
+ def send_blocking_command(command, timeout, &block)
50
+ future = Future.new(command, block, @exception)
51
+ @pipeline.blocking_call_v(timeout, command) do |result|
52
+ future._set(result)
204
53
  end
54
+ @futures << future
55
+ future
205
56
  end
206
57
  end
207
58
 
208
- class DeprecatedPipeline < DelegateClass(Pipeline)
209
- def initialize(pipeline)
210
- super(pipeline)
211
- @deprecation_displayed = false
59
+ class MultiConnection < PipelinedConnection
60
+ def multi
61
+ raise Redis::BaseError, "Can't nest multi transaction"
212
62
  end
213
63
 
214
- def __getobj__
215
- unless @deprecation_displayed
216
- Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 10))
217
- @deprecation_displayed = true
218
- end
219
- @delegate_dc_obj
220
- end
221
- end
222
-
223
- class DeprecatedMulti < DelegateClass(Pipeline::Multi)
224
- def initialize(pipeline)
225
- super(pipeline)
226
- @deprecation_displayed = false
227
- end
64
+ private
228
65
 
229
- def __getobj__
230
- unless @deprecation_displayed
231
- Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 10))
232
- @deprecation_displayed = true
233
- end
234
- @delegate_dc_obj
66
+ # Blocking commands inside transaction behave like non-blocking.
67
+ # It shouldn't be done though.
68
+ # https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction
69
+ def send_blocking_command(command, _timeout, &block)
70
+ send_command(command, &block)
235
71
  end
236
72
  end
237
73
 
@@ -244,23 +80,11 @@ class Redis
244
80
  class Future < BasicObject
245
81
  FutureNotReady = ::Redis::FutureNotReady.new
246
82
 
247
- attr_reader :timeout
248
-
249
- def initialize(command, transformation, timeout)
83
+ def initialize(command, coerce, exception)
250
84
  @command = command
251
- @transformation = transformation
252
- @timeout = timeout
253
85
  @object = FutureNotReady
254
- end
255
-
256
- def ==(_other)
257
- message = +"The methods == and != are deprecated for Redis::Future and will be removed in 5.0.0"
258
- message << " - You probably meant to call .value == or .value !="
259
- message << " (#{::Kernel.caller(1, 1).first})\n"
260
-
261
- ::Redis.deprecate!(message)
262
-
263
- super
86
+ @coerce = coerce
87
+ @exception = exception
264
88
  end
265
89
 
266
90
  def inspect
@@ -268,16 +92,12 @@ class Redis
268
92
  end
269
93
 
270
94
  def _set(object)
271
- @object = @transformation ? @transformation.call(object) : object
95
+ @object = @coerce ? @coerce.call(object) : object
272
96
  value
273
97
  end
274
98
 
275
- def _command
276
- @command
277
- end
278
-
279
99
  def value
280
- ::Kernel.raise(@object) if @object.is_a?(::RuntimeError)
100
+ ::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError)
281
101
  @object
282
102
  end
283
103
 
@@ -294,13 +114,18 @@ class Redis
294
114
  def initialize(futures)
295
115
  @futures = futures
296
116
  @command = [:exec]
117
+ @object = FutureNotReady
297
118
  end
298
119
 
299
120
  def _set(replies)
300
- @futures.each_with_index do |future, index|
301
- future._set(replies[index])
121
+ @object = if replies
122
+ @futures.map.with_index do |future, index|
123
+ future._set(replies[index])
124
+ future.value
125
+ end
126
+ else
127
+ replies
302
128
  end
303
- replies
304
129
  end
305
130
  end
306
131
  end
@@ -4,10 +4,13 @@ class Redis
4
4
  class SubscribedClient
5
5
  def initialize(client)
6
6
  @client = client
7
+ @write_monitor = Monitor.new
7
8
  end
8
9
 
9
- def call(command)
10
- @client.process([command])
10
+ def call_v(command)
11
+ @write_monitor.synchronize do
12
+ @client.call_v(command)
13
+ end
11
14
  end
12
15
 
13
16
  def subscribe(*channels, &block)
@@ -26,12 +29,28 @@ class Redis
26
29
  subscription("psubscribe", "punsubscribe", channels, block, timeout)
27
30
  end
28
31
 
32
+ def ssubscribe(*channels, &block)
33
+ subscription("ssubscribe", "sunsubscribe", channels, block)
34
+ end
35
+
36
+ def ssubscribe_with_timeout(timeout, *channels, &block)
37
+ subscription("ssubscribe", "sunsubscribe", channels, block, timeout)
38
+ end
39
+
29
40
  def unsubscribe(*channels)
30
- call([:unsubscribe, *channels])
41
+ call_v([:unsubscribe, *channels])
31
42
  end
32
43
 
33
44
  def punsubscribe(*channels)
34
- call([:punsubscribe, *channels])
45
+ call_v([:punsubscribe, *channels])
46
+ end
47
+
48
+ def sunsubscribe(*channels)
49
+ call_v([:sunsubscribe, *channels])
50
+ end
51
+
52
+ def close
53
+ @client.close
35
54
  end
36
55
 
37
56
  protected
@@ -39,13 +58,21 @@ class Redis
39
58
  def subscription(start, stop, channels, block, timeout = 0)
40
59
  sub = Subscription.new(&block)
41
60
 
42
- unsubscribed = false
61
+ case start
62
+ when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys
63
+ else call_v([start, *channels])
64
+ end
43
65
 
44
- @client.call_loop([start, *channels], timeout) do |line|
45
- type, *rest = line
46
- sub.callbacks[type].call(*rest)
47
- unsubscribed = type == stop && rest.last == 0
48
- break if unsubscribed
66
+ while event = @client.next_event(timeout)
67
+ if event.is_a?(::RedisClient::CommandError)
68
+ raise Client::ERROR_MAPPING.fetch(event.class), event.message
69
+ end
70
+
71
+ type, *rest = event
72
+ if callback = sub.callbacks[type]
73
+ callback.call(*rest)
74
+ end
75
+ break if type == stop && rest.last == 0
49
76
  end
50
77
  # No need to unsubscribe here. The real client closes the connection
51
78
  # whenever an exception is raised (see #ensure_connected).
@@ -56,10 +83,7 @@ class Redis
56
83
  attr :callbacks
57
84
 
58
85
  def initialize
59
- @callbacks = Hash.new do |hash, key|
60
- hash[key] = ->(*_) {}
61
- end
62
-
86
+ @callbacks = {}
63
87
  yield(self)
64
88
  end
65
89
 
@@ -86,5 +110,17 @@ class Redis
86
110
  def pmessage(&block)
87
111
  @callbacks["pmessage"] = block
88
112
  end
113
+
114
+ def ssubscribe(&block)
115
+ @callbacks["ssubscribe"] = block
116
+ end
117
+
118
+ def sunsubscribe(&block)
119
+ @callbacks["sunsubscribe"] = block
120
+ end
121
+
122
+ def smessage(&block)
123
+ @callbacks["smessage"] = block
124
+ end
89
125
  end
90
126
  end
data/lib/redis/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Redis
4
- VERSION = '4.8.1'
4
+ VERSION = '5.4.0'
5
5
  end