redis 4.4.0 → 4.8.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 (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