redis 4.4.0 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/README.md +25 -10
  4. data/lib/redis/client.rb +31 -25
  5. data/lib/redis/cluster/command.rb +4 -6
  6. data/lib/redis/cluster/command_loader.rb +8 -9
  7. data/lib/redis/cluster/node.rb +12 -0
  8. data/lib/redis/cluster/node_loader.rb +8 -11
  9. data/lib/redis/cluster/option.rb +10 -3
  10. data/lib/redis/cluster/slot_loader.rb +9 -12
  11. data/lib/redis/cluster.rb +24 -0
  12. data/lib/redis/commands/bitmaps.rb +63 -0
  13. data/lib/redis/commands/cluster.rb +45 -0
  14. data/lib/redis/commands/connection.rb +58 -0
  15. data/lib/redis/commands/geo.rb +84 -0
  16. data/lib/redis/commands/hashes.rb +251 -0
  17. data/lib/redis/commands/hyper_log_log.rb +37 -0
  18. data/lib/redis/commands/keys.rb +455 -0
  19. data/lib/redis/commands/lists.rb +290 -0
  20. data/lib/redis/commands/pubsub.rb +72 -0
  21. data/lib/redis/commands/scripting.rb +114 -0
  22. data/lib/redis/commands/server.rb +188 -0
  23. data/lib/redis/commands/sets.rb +223 -0
  24. data/lib/redis/commands/sorted_sets.rb +812 -0
  25. data/lib/redis/commands/streams.rb +382 -0
  26. data/lib/redis/commands/strings.rb +313 -0
  27. data/lib/redis/commands/transactions.rb +139 -0
  28. data/lib/redis/commands.rb +240 -0
  29. data/lib/redis/connection/command_helper.rb +2 -0
  30. data/lib/redis/connection/hiredis.rb +3 -2
  31. data/lib/redis/connection/ruby.rb +19 -9
  32. data/lib/redis/connection/synchrony.rb +10 -8
  33. data/lib/redis/connection.rb +1 -1
  34. data/lib/redis/distributed.rb +111 -23
  35. data/lib/redis/errors.rb +9 -0
  36. data/lib/redis/pipeline.rb +128 -3
  37. data/lib/redis/version.rb +1 -1
  38. data/lib/redis.rb +138 -3482
  39. metadata +22 -5
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Transactions
6
+ # Mark the start of a transaction block.
7
+ #
8
+ # Passing a block is optional.
9
+ #
10
+ # @example With a block
11
+ # redis.multi do |multi|
12
+ # multi.set("key", "value")
13
+ # multi.incr("counter")
14
+ # end # => ["OK", 6]
15
+ #
16
+ # @example Without a block
17
+ # redis.multi
18
+ # # => "OK"
19
+ # redis.set("key", "value")
20
+ # # => "QUEUED"
21
+ # redis.incr("counter")
22
+ # # => "QUEUED"
23
+ # redis.exec
24
+ # # => ["OK", 6]
25
+ #
26
+ # @yield [multi] the commands that are called inside this block are cached
27
+ # and written to the server upon returning from it
28
+ # @yieldparam [Redis] multi `self`
29
+ #
30
+ # @return [String, Array<...>]
31
+ # - when a block is not given, `OK`
32
+ # - when a block is given, an array with replies
33
+ #
34
+ # @see #watch
35
+ # @see #unwatch
36
+ def multi(&block) # :nodoc:
37
+ if block_given?
38
+ if block&.arity == 0
39
+ Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
40
+ end
41
+
42
+ synchronize do |prior_client|
43
+ pipeline = Pipeline::Multi.new(prior_client)
44
+ pipelined_connection = PipelinedConnection.new(pipeline)
45
+ yield pipelined_connection
46
+ prior_client.call_pipeline(pipeline)
47
+ end
48
+ else
49
+ send_command([:multi])
50
+ end
51
+ end
52
+
53
+ # Watch the given keys to determine execution of the MULTI/EXEC block.
54
+ #
55
+ # Using a block is optional, but is necessary for thread-safety.
56
+ #
57
+ # An `#unwatch` is automatically issued if an exception is raised within the
58
+ # block that is a subclass of StandardError and is not a ConnectionError.
59
+ #
60
+ # @example With a block
61
+ # redis.watch("key") do
62
+ # if redis.get("key") == "some value"
63
+ # redis.multi do |multi|
64
+ # multi.set("key", "other value")
65
+ # multi.incr("counter")
66
+ # end
67
+ # else
68
+ # redis.unwatch
69
+ # end
70
+ # end
71
+ # # => ["OK", 6]
72
+ #
73
+ # @example Without a block
74
+ # redis.watch("key")
75
+ # # => "OK"
76
+ #
77
+ # @param [String, Array<String>] keys one or more keys to watch
78
+ # @return [Object] if using a block, returns the return value of the block
79
+ # @return [String] if not using a block, returns `OK`
80
+ #
81
+ # @see #unwatch
82
+ # @see #multi
83
+ def watch(*keys)
84
+ synchronize do |client|
85
+ res = client.call([:watch, *keys])
86
+
87
+ if block_given?
88
+ begin
89
+ yield(self)
90
+ rescue ConnectionError
91
+ raise
92
+ rescue StandardError
93
+ unwatch
94
+ raise
95
+ end
96
+ else
97
+ res
98
+ end
99
+ end
100
+ end
101
+
102
+ # Forget about all watched keys.
103
+ #
104
+ # @return [String] `OK`
105
+ #
106
+ # @see #watch
107
+ # @see #multi
108
+ def unwatch
109
+ send_command([:unwatch])
110
+ end
111
+
112
+ # Execute all commands issued after MULTI.
113
+ #
114
+ # Only call this method when `#multi` was called **without** a block.
115
+ #
116
+ # @return [nil, Array<...>]
117
+ # - when commands were not executed, `nil`
118
+ # - when commands were executed, an array with their replies
119
+ #
120
+ # @see #multi
121
+ # @see #discard
122
+ def exec
123
+ send_command([:exec])
124
+ end
125
+
126
+ # Discard all commands issued after MULTI.
127
+ #
128
+ # Only call this method when `#multi` was called **without** a block.
129
+ #
130
+ # @return [String] `"OK"`
131
+ #
132
+ # @see #multi
133
+ # @see #exec
134
+ def discard
135
+ send_command([:discard])
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis/commands/bitmaps"
4
+ require "redis/commands/cluster"
5
+ require "redis/commands/connection"
6
+ require "redis/commands/geo"
7
+ require "redis/commands/hashes"
8
+ require "redis/commands/hyper_log_log"
9
+ require "redis/commands/keys"
10
+ require "redis/commands/lists"
11
+ require "redis/commands/pubsub"
12
+ require "redis/commands/scripting"
13
+ require "redis/commands/server"
14
+ require "redis/commands/sets"
15
+ require "redis/commands/sorted_sets"
16
+ require "redis/commands/streams"
17
+ require "redis/commands/strings"
18
+ require "redis/commands/transactions"
19
+
20
+ class Redis
21
+ module Commands
22
+ include Bitmaps
23
+ include Cluster
24
+ include Connection
25
+ include Geo
26
+ include Hashes
27
+ include HyperLogLog
28
+ include Keys
29
+ include Lists
30
+ include Pubsub
31
+ include Scripting
32
+ include Server
33
+ include Sets
34
+ include SortedSets
35
+ include Streams
36
+ include Strings
37
+ include Transactions
38
+
39
+ # Commands returning 1 for true and 0 for false may be executed in a pipeline
40
+ # where the method call will return nil. Propagate the nil instead of falsely
41
+ # returning false.
42
+ Boolify = lambda { |value|
43
+ case value
44
+ when Integer
45
+ value > 0
46
+ else
47
+ value
48
+ end
49
+ }
50
+
51
+ BoolifySet = lambda { |value|
52
+ case value
53
+ when "OK"
54
+ true
55
+ when nil
56
+ false
57
+ else
58
+ value
59
+ end
60
+ }
61
+
62
+ Hashify = lambda { |value|
63
+ if value.respond_to?(:each_slice)
64
+ value.each_slice(2).to_h
65
+ else
66
+ value
67
+ end
68
+ }
69
+
70
+ Pairify = lambda { |value|
71
+ if value.respond_to?(:each_slice)
72
+ value.each_slice(2).to_a
73
+ else
74
+ value
75
+ end
76
+ }
77
+
78
+ Floatify = lambda { |value|
79
+ case value
80
+ when "inf"
81
+ Float::INFINITY
82
+ when "-inf"
83
+ -Float::INFINITY
84
+ when String
85
+ Float(value)
86
+ else
87
+ value
88
+ end
89
+ }
90
+
91
+ FloatifyPairs = lambda { |value|
92
+ return value unless value.respond_to?(:each_slice)
93
+
94
+ value.each_slice(2).map do |member, score|
95
+ [member, Floatify.call(score)]
96
+ end
97
+ }
98
+
99
+ HashifyInfo = lambda { |reply|
100
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
101
+ lines.map! { |line| line.split(':', 2) }
102
+ lines.compact!
103
+ lines.to_h
104
+ }
105
+
106
+ HashifyStreams = lambda { |reply|
107
+ case reply
108
+ when nil
109
+ {}
110
+ else
111
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
112
+ end
113
+ }
114
+
115
+ EMPTY_STREAM_RESPONSE = [nil].freeze
116
+ private_constant :EMPTY_STREAM_RESPONSE
117
+
118
+ HashifyStreamEntries = lambda { |reply|
119
+ reply.compact.map do |entry_id, values|
120
+ [entry_id, values&.each_slice(2)&.to_h]
121
+ end
122
+ }
123
+
124
+ HashifyStreamAutoclaim = lambda { |reply|
125
+ {
126
+ 'next' => reply[0],
127
+ 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
128
+ }
129
+ }
130
+
131
+ HashifyStreamAutoclaimJustId = lambda { |reply|
132
+ {
133
+ 'next' => reply[0],
134
+ 'entries' => reply[1]
135
+ }
136
+ }
137
+
138
+ HashifyStreamPendings = lambda { |reply|
139
+ {
140
+ 'size' => reply[0],
141
+ 'min_entry_id' => reply[1],
142
+ 'max_entry_id' => reply[2],
143
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
144
+ }
145
+ }
146
+
147
+ HashifyStreamPendingDetails = lambda { |reply|
148
+ reply.map do |arr|
149
+ {
150
+ 'entry_id' => arr[0],
151
+ 'consumer' => arr[1],
152
+ 'elapsed' => arr[2],
153
+ 'count' => arr[3]
154
+ }
155
+ end
156
+ }
157
+
158
+ HashifyClusterNodeInfo = lambda { |str|
159
+ arr = str.split(' ')
160
+ {
161
+ 'node_id' => arr[0],
162
+ 'ip_port' => arr[1],
163
+ 'flags' => arr[2].split(','),
164
+ 'master_node_id' => arr[3],
165
+ 'ping_sent' => arr[4],
166
+ 'pong_recv' => arr[5],
167
+ 'config_epoch' => arr[6],
168
+ 'link_state' => arr[7],
169
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
170
+ }
171
+ }
172
+
173
+ HashifyClusterSlots = lambda { |reply|
174
+ reply.map do |arr|
175
+ first_slot, last_slot = arr[0..1]
176
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
177
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
178
+ {
179
+ 'start_slot' => first_slot,
180
+ 'end_slot' => last_slot,
181
+ 'master' => master,
182
+ 'replicas' => replicas
183
+ }
184
+ end
185
+ }
186
+
187
+ HashifyClusterNodes = lambda { |reply|
188
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
189
+ }
190
+
191
+ HashifyClusterSlaves = lambda { |reply|
192
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
193
+ }
194
+
195
+ Noop = ->(reply) { reply }
196
+
197
+ # Sends a command to Redis and returns its reply.
198
+ #
199
+ # Replies are converted to Ruby objects according to the RESP protocol, so
200
+ # you can expect a Ruby array, integer or nil when Redis sends one. Higher
201
+ # level transformations, such as converting an array of pairs into a Ruby
202
+ # hash, are up to consumers.
203
+ #
204
+ # Redis error replies are raised as Ruby exceptions.
205
+ def call(*command)
206
+ send_command(command)
207
+ end
208
+
209
+ # Interact with the sentinel command (masters, master, slaves, failover)
210
+ #
211
+ # @param [String] subcommand e.g. `masters`, `master`, `slaves`
212
+ # @param [Array<String>] args depends on subcommand
213
+ # @return [Array<String>, Hash<String, String>, String] depends on subcommand
214
+ def sentinel(subcommand, *args)
215
+ subcommand = subcommand.to_s.downcase
216
+ send_command([:sentinel, subcommand] + args) do |reply|
217
+ case subcommand
218
+ when "get-master-addr-by-name"
219
+ reply
220
+ else
221
+ if reply.is_a?(Array)
222
+ if reply[0].is_a?(Array)
223
+ reply.map(&Hashify)
224
+ else
225
+ Hashify.call(reply)
226
+ end
227
+ else
228
+ reply
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def method_missing(*command) # rubocop:disable Style/MissingRespondToMissing
237
+ send_command(command)
238
+ end
239
+ end
240
+ end
@@ -12,11 +12,13 @@ class Redis
12
12
  if i.is_a? Array
13
13
  i.each do |j|
14
14
  j = j.to_s
15
+ j = j.encoding == Encoding::BINARY ? j : j.b
15
16
  command << "$#{j.bytesize}"
16
17
  command << j
17
18
  end
18
19
  else
19
20
  i = i.to_s
21
+ i = i.encoding == Encoding::BINARY ? i : i.b
20
22
  command << "$#{i.bytesize}"
21
23
  command << i
22
24
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "registry"
4
- require_relative "../errors"
3
+ require "redis/connection/registry"
4
+ require "redis/errors"
5
+
5
6
  require "hiredis/connection"
6
7
  require "timeout"
7
8
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "registry"
4
- require_relative "command_helper"
5
- require_relative "../errors"
3
+ require "redis/connection/registry"
4
+ require "redis/connection/command_helper"
5
+ require "redis/errors"
6
+
6
7
  require "socket"
7
8
  require "timeout"
8
9
 
@@ -21,7 +22,7 @@ class Redis
21
22
  super(*args)
22
23
 
23
24
  @timeout = @write_timeout = nil
24
- @buffer = "".dup
25
+ @buffer = "".b
25
26
  end
26
27
 
27
28
  def timeout=(timeout)
@@ -35,7 +36,8 @@ class Redis
35
36
  def read(nbytes)
36
37
  result = @buffer.slice!(0, nbytes)
37
38
 
38
- result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
39
+ buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
40
+ result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
39
41
 
40
42
  result
41
43
  end
@@ -48,9 +50,9 @@ class Redis
48
50
  @buffer.slice!(0, crlf + CRLF.bytesize)
49
51
  end
50
52
 
51
- def _read_from_socket(nbytes)
53
+ def _read_from_socket(nbytes, buffer = nil)
52
54
  loop do
53
- case chunk = read_nonblock(nbytes, exception: false)
55
+ case chunk = read_nonblock(nbytes, buffer, exception: false)
54
56
  when :wait_readable
55
57
  unless wait_readable(@timeout)
56
58
  raise Redis::TimeoutError
@@ -132,7 +134,9 @@ class Redis
132
134
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
133
135
  # Use the blocking #readpartial method instead.
134
136
 
135
- def _read_from_socket(nbytes)
137
+ def _read_from_socket(nbytes, _buffer = nil)
138
+ # JRuby: Throw away the buffer as we won't need it
139
+ # but still need to support the max arity of 2
136
140
  readpartial(nbytes)
137
141
  rescue EOFError
138
142
  raise Errno::ECONNRESET
@@ -239,7 +243,7 @@ class Redis
239
243
  end
240
244
 
241
245
  def self.connect(host, port, timeout, ssl_params)
242
- # Note: this is using Redis::Connection::TCPSocket
246
+ # NOTE: this is using Redis::Connection::TCPSocket
243
247
  tcp_sock = TCPSocket.connect(host, port, timeout)
244
248
 
245
249
  ctx = OpenSSL::SSL::SSLContext.new
@@ -380,6 +384,12 @@ class Redis
380
384
  format_reply(reply_type, line)
381
385
  rescue Errno::EAGAIN
382
386
  raise TimeoutError
387
+ rescue OpenSSL::SSL::SSLError => ssl_error
388
+ if ssl_error.message.match?(/SSL_read: unexpected eof while reading/i)
389
+ raise EOFError, ssl_error.message
390
+ else
391
+ raise
392
+ end
383
393
  end
384
394
 
385
395
  def format_reply(reply_type, line)
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "command_helper"
4
- require_relative "registry"
5
- require_relative "../errors"
3
+ require "redis/connection/registry"
4
+ require "redis/connection/command_helper"
5
+ require "redis/errors"
6
+
6
7
  require "em-synchrony"
7
8
  require "hiredis/reader"
8
9
 
9
- Kernel.warn(
10
- "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0. " \
10
+ ::Redis.deprecate!(
11
+ "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0.0. " \
11
12
  "We're looking for people to maintain it as a separate gem, see https://github.com/redis/redis-rb/issues/915"
12
13
  )
13
14
 
@@ -129,11 +130,12 @@ class Redis
129
130
  def read
130
131
  type, payload = @connection.read
131
132
 
132
- if type == :reply
133
+ case type
134
+ when :reply
133
135
  payload
134
- elsif type == :error
136
+ when :error
135
137
  raise payload
136
- elsif type == :timeout
138
+ when :timeout
137
139
  raise TimeoutError
138
140
  else
139
141
  raise "Unknown type #{type.inspect}"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "connection/registry"
3
+ require "redis/connection/registry"
4
4
 
5
5
  # If a connection driver was required before this file, the array
6
6
  # Redis::Connection.drivers will contain one or more classes. The last driver