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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +125 -162
- data/lib/redis/client.rb +82 -616
- data/lib/redis/commands/bitmaps.rb +14 -4
- data/lib/redis/commands/cluster.rb +1 -18
- data/lib/redis/commands/connection.rb +5 -10
- data/lib/redis/commands/geo.rb +3 -3
- data/lib/redis/commands/hashes.rb +13 -6
- data/lib/redis/commands/hyper_log_log.rb +1 -1
- data/lib/redis/commands/keys.rb +27 -23
- data/lib/redis/commands/lists.rb +74 -25
- data/lib/redis/commands/pubsub.rb +34 -25
- data/lib/redis/commands/server.rb +15 -15
- data/lib/redis/commands/sets.rb +35 -40
- data/lib/redis/commands/sorted_sets.rb +128 -18
- data/lib/redis/commands/streams.rb +48 -21
- data/lib/redis/commands/strings.rb +18 -17
- data/lib/redis/commands/transactions.rb +7 -31
- data/lib/redis/commands.rb +11 -12
- data/lib/redis/distributed.rb +136 -72
- data/lib/redis/errors.rb +15 -50
- data/lib/redis/hash_ring.rb +26 -26
- data/lib/redis/pipeline.rb +47 -222
- data/lib/redis/subscribe.rb +50 -14
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +77 -184
- metadata +10 -57
- data/lib/redis/cluster/command.rb +0 -79
- data/lib/redis/cluster/command_loader.rb +0 -33
- data/lib/redis/cluster/key_slot_converter.rb +0 -72
- data/lib/redis/cluster/node.rb +0 -120
- data/lib/redis/cluster/node_key.rb +0 -31
- data/lib/redis/cluster/node_loader.rb +0 -34
- data/lib/redis/cluster/option.rb +0 -100
- data/lib/redis/cluster/slot.rb +0 -86
- data/lib/redis/cluster/slot_loader.rb +0 -46
- data/lib/redis/cluster.rb +0 -315
- data/lib/redis/connection/command_helper.rb +0 -41
- data/lib/redis/connection/hiredis.rb +0 -68
- data/lib/redis/connection/registry.rb +0 -13
- data/lib/redis/connection/ruby.rb +0 -437
- data/lib/redis/connection/synchrony.rb +0 -148
- 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 <
|
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
|
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
|
data/lib/redis/hash_ring.rb
CHANGED
@@ -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 =
|
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 =
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|
68
|
-
upper = ary.size
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
upper =
|
77
|
+
while lower < upper
|
78
|
+
mid = (lower + upper) / 2
|
79
|
+
if ary[mid] > value
|
80
|
+
upper = mid
|
80
81
|
else
|
81
|
-
lower =
|
82
|
+
lower = mid + 1
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
|
-
upper
|
86
|
-
upper
|
86
|
+
upper - 1
|
87
87
|
end
|
88
88
|
end
|
89
89
|
end
|
data/lib/redis/pipeline.rb
CHANGED
@@ -4,27 +4,31 @@ require "delegate"
|
|
4
4
|
|
5
5
|
class Redis
|
6
6
|
class PipelinedConnection
|
7
|
-
|
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
|
26
|
-
|
27
|
-
|
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
|
-
|
38
|
-
|
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
|
116
|
-
|
117
|
-
|
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
|
209
|
-
def
|
210
|
-
|
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
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
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
|
-
|
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 = @
|
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?(::
|
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
|
-
@
|
301
|
-
future
|
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
|
data/lib/redis/subscribe.rb
CHANGED
@@ -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
|
10
|
-
@
|
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
|
-
|
41
|
+
call_v([:unsubscribe, *channels])
|
31
42
|
end
|
32
43
|
|
33
44
|
def punsubscribe(*channels)
|
34
|
-
|
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
|
-
|
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.
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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 =
|
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