redis 4.8.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +101 -161
  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 +9 -6
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +21 -23
  12. data/lib/redis/commands/lists.rb +74 -25
  13. data/lib/redis/commands/pubsub.rb +28 -25
  14. data/lib/redis/commands/server.rb +15 -15
  15. data/lib/redis/commands/sets.rb +31 -40
  16. data/lib/redis/commands/sorted_sets.rb +84 -12
  17. data/lib/redis/commands/streams.rb +39 -19
  18. data/lib/redis/commands/strings.rb +18 -17
  19. data/lib/redis/commands/transactions.rb +7 -31
  20. data/lib/redis/commands.rb +4 -7
  21. data/lib/redis/distributed.rb +128 -68
  22. data/lib/redis/errors.rb +15 -50
  23. data/lib/redis/hash_ring.rb +26 -26
  24. data/lib/redis/pipeline.rb +43 -222
  25. data/lib/redis/subscribe.rb +50 -14
  26. data/lib/redis/version.rb +1 -1
  27. data/lib/redis.rb +76 -184
  28. metadata +10 -54
  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
@@ -4,27 +4,30 @@ 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 = [])
8
10
  @pipeline = pipeline
11
+ @futures = futures
9
12
  end
10
13
 
11
14
  include Commands
12
15
 
13
- def db
14
- @pipeline.db
15
- end
16
-
17
- def db=(db)
18
- @pipeline.db = db
19
- end
20
-
21
16
  def pipelined
22
17
  yield self
23
18
  end
24
19
 
25
- def call_pipeline(pipeline)
26
- @pipeline.call_pipeline(pipeline)
27
- nil
20
+ def multi
21
+ transaction = MultiConnection.new(@pipeline, @futures)
22
+ send_command([:multi])
23
+ size = @futures.size
24
+ yield transaction
25
+ multi_future = MultiFuture.new(@futures[size..-1])
26
+ @pipeline.call_v([:exec]) do |result|
27
+ multi_future._set(result)
28
+ end
29
+ @futures << multi_future
30
+ multi_future
28
31
  end
29
32
 
30
33
  private
@@ -34,204 +37,36 @@ class Redis
34
37
  end
35
38
 
36
39
  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
40
+ future = Future.new(command, block)
41
+ @pipeline.call_v(command) do |result|
42
+ future._set(result)
70
43
  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
44
  @futures << future
112
45
  future
113
46
  end
114
47
 
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
48
+ def send_blocking_command(command, timeout, &block)
49
+ future = Future.new(command, block)
50
+ @pipeline.blocking_call_v(timeout, command) do |result|
51
+ future._set(result)
204
52
  end
53
+ @futures << future
54
+ future
205
55
  end
206
56
  end
207
57
 
208
- class DeprecatedPipeline < DelegateClass(Pipeline)
209
- def initialize(pipeline)
210
- super(pipeline)
211
- @deprecation_displayed = false
58
+ class MultiConnection < PipelinedConnection
59
+ def multi
60
+ raise Redis::Error, "Can't nest multi transaction"
212
61
  end
213
62
 
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
63
+ private
222
64
 
223
- class DeprecatedMulti < DelegateClass(Pipeline::Multi)
224
- def initialize(pipeline)
225
- super(pipeline)
226
- @deprecation_displayed = false
227
- end
228
-
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
65
+ # Blocking commands inside transaction behave like non-blocking.
66
+ # It shouldn't be done though.
67
+ # https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction
68
+ def send_blocking_command(command, _timeout, &block)
69
+ send_command(command, &block)
235
70
  end
236
71
  end
237
72
 
@@ -244,23 +79,10 @@ class Redis
244
79
  class Future < BasicObject
245
80
  FutureNotReady = ::Redis::FutureNotReady.new
246
81
 
247
- attr_reader :timeout
248
-
249
- def initialize(command, transformation, timeout)
82
+ def initialize(command, coerce)
250
83
  @command = command
251
- @transformation = transformation
252
- @timeout = timeout
253
84
  @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
85
+ @coerce = coerce
264
86
  end
265
87
 
266
88
  def inspect
@@ -268,16 +90,12 @@ class Redis
268
90
  end
269
91
 
270
92
  def _set(object)
271
- @object = @transformation ? @transformation.call(object) : object
93
+ @object = @coerce ? @coerce.call(object) : object
272
94
  value
273
95
  end
274
96
 
275
- def _command
276
- @command
277
- end
278
-
279
97
  def value
280
- ::Kernel.raise(@object) if @object.is_a?(::RuntimeError)
98
+ ::Kernel.raise(@object) if @object.is_a?(::StandardError)
281
99
  @object
282
100
  end
283
101
 
@@ -294,13 +112,16 @@ class Redis
294
112
  def initialize(futures)
295
113
  @futures = futures
296
114
  @command = [:exec]
115
+ @object = FutureNotReady
297
116
  end
298
117
 
299
118
  def _set(replies)
300
- @futures.each_with_index do |future, index|
301
- future._set(replies[index])
119
+ if replies
120
+ @futures.each_with_index do |future, index|
121
+ future._set(replies[index])
122
+ end
302
123
  end
303
- replies
124
+ @object = replies
304
125
  end
305
126
  end
306
127
  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.1.0'
5
5
  end