redis 4.3.1 → 5.4.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +190 -0
  3. data/README.md +132 -154
  4. data/lib/redis/client.rb +81 -600
  5. data/lib/redis/commands/bitmaps.rb +73 -0
  6. data/lib/redis/commands/cluster.rb +28 -0
  7. data/lib/redis/commands/connection.rb +53 -0
  8. data/lib/redis/commands/geo.rb +84 -0
  9. data/lib/redis/commands/hashes.rb +258 -0
  10. data/lib/redis/commands/hyper_log_log.rb +37 -0
  11. data/lib/redis/commands/keys.rb +459 -0
  12. data/lib/redis/commands/lists.rb +339 -0
  13. data/lib/redis/commands/pubsub.rb +81 -0
  14. data/lib/redis/commands/scripting.rb +114 -0
  15. data/lib/redis/commands/server.rb +188 -0
  16. data/lib/redis/commands/sets.rb +218 -0
  17. data/lib/redis/commands/sorted_sets.rb +922 -0
  18. data/lib/redis/commands/streams.rb +409 -0
  19. data/lib/redis/commands/strings.rb +314 -0
  20. data/lib/redis/commands/transactions.rb +115 -0
  21. data/lib/redis/commands.rb +239 -0
  22. data/lib/redis/distributed.rb +236 -77
  23. data/lib/redis/errors.rb +20 -41
  24. data/lib/redis/hash_ring.rb +26 -26
  25. data/lib/redis/pipeline.rb +70 -120
  26. data/lib/redis/subscribe.rb +50 -14
  27. data/lib/redis/version.rb +1 -1
  28. data/lib/redis.rb +104 -3465
  29. metadata +27 -57
  30. data/lib/redis/cluster/command.rb +0 -81
  31. data/lib/redis/cluster/command_loader.rb +0 -34
  32. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  33. data/lib/redis/cluster/node.rb +0 -107
  34. data/lib/redis/cluster/node_key.rb +0 -31
  35. data/lib/redis/cluster/node_loader.rb +0 -37
  36. data/lib/redis/cluster/option.rb +0 -93
  37. data/lib/redis/cluster/slot.rb +0 -86
  38. data/lib/redis/cluster/slot_loader.rb +0 -49
  39. data/lib/redis/cluster.rb +0 -295
  40. data/lib/redis/connection/command_helper.rb +0 -39
  41. data/lib/redis/connection/hiredis.rb +0 -67
  42. data/lib/redis/connection/registry.rb +0 -13
  43. data/lib/redis/connection/ruby.rb +0 -427
  44. data/lib/redis/connection/synchrony.rb +0 -146
  45. data/lib/redis/connection.rb +0 -11
@@ -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
@@ -1,126 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+
3
5
  class Redis
4
- class Pipeline
6
+ class PipelinedConnection
5
7
  attr_accessor :db
6
- attr_reader :client
7
-
8
- attr :futures
9
8
 
10
- def initialize(client)
11
- @client = client.is_a?(Pipeline) ? client.client : client
12
- @with_reconnect = true
13
- @shutdown = false
14
- @futures = []
9
+ def initialize(pipeline, futures = [], exception: true)
10
+ @pipeline = pipeline
11
+ @futures = futures
12
+ @exception = exception
15
13
  end
16
14
 
17
- def timeout
18
- client.timeout
19
- end
15
+ include Commands
20
16
 
21
- def with_reconnect?
22
- @with_reconnect
17
+ def pipelined
18
+ yield self
23
19
  end
24
20
 
25
- def without_reconnect?
26
- !@with_reconnect
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
27
32
  end
28
33
 
29
- def shutdown?
30
- @shutdown
31
- end
34
+ private
32
35
 
33
- def empty?
34
- @futures.empty?
36
+ def synchronize
37
+ yield self
35
38
  end
36
39
 
37
- def call(command, timeout: nil, &block)
38
- # A pipeline that contains a shutdown should not raise ECONNRESET when
39
- # the connection is gone.
40
- @shutdown = true if command.first == :shutdown
41
- future = Future.new(command, block, timeout)
40
+ def send_command(command, &block)
41
+ future = Future.new(command, block, @exception)
42
+ @pipeline.call_v(command) do |result|
43
+ future._set(result)
44
+ end
42
45
  @futures << future
43
46
  future
44
47
  end
45
48
 
46
- def call_with_timeout(command, timeout, &block)
47
- call(command, timeout: timeout, &block)
48
- end
49
-
50
- def call_pipeline(pipeline)
51
- @shutdown = true if pipeline.shutdown?
52
- @futures.concat(pipeline.futures)
53
- @db = pipeline.db
54
- nil
55
- end
56
-
57
- def commands
58
- @futures.map(&:_command)
59
- end
60
-
61
- def timeouts
62
- @futures.map(&:timeout)
63
- end
64
-
65
- def with_reconnect(val = true)
66
- @with_reconnect = false unless val
67
- yield
68
- end
69
-
70
- def without_reconnect(&blk)
71
- with_reconnect(false, &blk)
72
- end
73
-
74
- def finish(replies, &blk)
75
- if blk
76
- futures.each_with_index.map do |future, i|
77
- future._set(blk.call(replies[i]))
78
- end
79
- else
80
- futures.each_with_index.map do |future, i|
81
- future._set(replies[i])
82
- 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)
83
53
  end
54
+ @futures << future
55
+ future
84
56
  end
57
+ end
85
58
 
86
- class Multi < self
87
- def finish(replies)
88
- exec = replies.last
89
-
90
- return if exec.nil? # The transaction failed because of WATCH.
91
-
92
- # EXEC command failed.
93
- raise exec if exec.is_a?(CommandError)
94
-
95
- if exec.size < futures.size
96
- # Some command wasn't recognized by Redis.
97
- command_error = replies.detect { |r| r.is_a?(CommandError) }
98
- raise command_error
99
- end
59
+ class MultiConnection < PipelinedConnection
60
+ def multi
61
+ raise Redis::BaseError, "Can't nest multi transaction"
62
+ end
100
63
 
101
- super(exec) do |reply|
102
- # Because an EXEC returns nested replies, hiredis won't be able to
103
- # convert an error reply to a CommandError instance itself. This is
104
- # specific to MULTI/EXEC, so we solve this here.
105
- reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
106
- end
107
- end
64
+ private
108
65
 
109
- def timeouts
110
- if empty?
111
- []
112
- else
113
- [nil, *super, nil]
114
- end
115
- end
116
-
117
- def commands
118
- if empty?
119
- []
120
- else
121
- [[:multi]] + super + [[:exec]]
122
- end
123
- end
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)
124
71
  end
125
72
  end
126
73
 
@@ -133,23 +80,11 @@ class Redis
133
80
  class Future < BasicObject
134
81
  FutureNotReady = ::Redis::FutureNotReady.new
135
82
 
136
- attr_reader :timeout
137
-
138
- def initialize(command, transformation, timeout)
83
+ def initialize(command, coerce, exception)
139
84
  @command = command
140
- @transformation = transformation
141
- @timeout = timeout
142
85
  @object = FutureNotReady
143
- end
144
-
145
- def ==(_other)
146
- message = +"The methods == and != are deprecated for Redis::Future and will be removed in 4.2.0"
147
- message << " - You probably meant to call .value == or .value !="
148
- message << " (#{::Kernel.caller(1, 1).first})\n"
149
-
150
- ::Kernel.warn(message)
151
-
152
- super
86
+ @coerce = coerce
87
+ @exception = exception
153
88
  end
154
89
 
155
90
  def inspect
@@ -157,16 +92,12 @@ class Redis
157
92
  end
158
93
 
159
94
  def _set(object)
160
- @object = @transformation ? @transformation.call(object) : object
95
+ @object = @coerce ? @coerce.call(object) : object
161
96
  value
162
97
  end
163
98
 
164
- def _command
165
- @command
166
- end
167
-
168
99
  def value
169
- ::Kernel.raise(@object) if @object.is_a?(::RuntimeError)
100
+ ::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError)
170
101
  @object
171
102
  end
172
103
 
@@ -178,4 +109,23 @@ class Redis
178
109
  Future
179
110
  end
180
111
  end
112
+
113
+ class MultiFuture < Future
114
+ def initialize(futures)
115
+ @futures = futures
116
+ @command = [:exec]
117
+ @object = FutureNotReady
118
+ end
119
+
120
+ def _set(replies)
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
128
+ end
129
+ end
130
+ end
181
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.3.1'
4
+ VERSION = '5.4.1'
5
5
  end