redis 4.5.0 → 4.7.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.
@@ -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,242 @@
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 1
45
+ true
46
+ when 0
47
+ false
48
+ else
49
+ value
50
+ end
51
+ }
52
+
53
+ BoolifySet = lambda { |value|
54
+ case value
55
+ when "OK"
56
+ true
57
+ when nil
58
+ false
59
+ else
60
+ value
61
+ end
62
+ }
63
+
64
+ Hashify = lambda { |value|
65
+ if value.respond_to?(:each_slice)
66
+ value.each_slice(2).to_h
67
+ else
68
+ value
69
+ end
70
+ }
71
+
72
+ Pairify = lambda { |value|
73
+ if value.respond_to?(:each_slice)
74
+ value.each_slice(2).to_a
75
+ else
76
+ value
77
+ end
78
+ }
79
+
80
+ Floatify = lambda { |value|
81
+ case value
82
+ when "inf"
83
+ Float::INFINITY
84
+ when "-inf"
85
+ -Float::INFINITY
86
+ when String
87
+ Float(value)
88
+ else
89
+ value
90
+ end
91
+ }
92
+
93
+ FloatifyPairs = lambda { |value|
94
+ return value unless value.respond_to?(:each_slice)
95
+
96
+ value.each_slice(2).map do |member, score|
97
+ [member, Floatify.call(score)]
98
+ end
99
+ }
100
+
101
+ HashifyInfo = lambda { |reply|
102
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
103
+ lines.map! { |line| line.split(':', 2) }
104
+ lines.compact!
105
+ lines.to_h
106
+ }
107
+
108
+ HashifyStreams = lambda { |reply|
109
+ case reply
110
+ when nil
111
+ {}
112
+ else
113
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
114
+ end
115
+ }
116
+
117
+ EMPTY_STREAM_RESPONSE = [nil].freeze
118
+ private_constant :EMPTY_STREAM_RESPONSE
119
+
120
+ HashifyStreamEntries = lambda { |reply|
121
+ reply.compact.map do |entry_id, values|
122
+ [entry_id, values&.each_slice(2)&.to_h]
123
+ end
124
+ }
125
+
126
+ HashifyStreamAutoclaim = lambda { |reply|
127
+ {
128
+ 'next' => reply[0],
129
+ 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
130
+ }
131
+ }
132
+
133
+ HashifyStreamAutoclaimJustId = lambda { |reply|
134
+ {
135
+ 'next' => reply[0],
136
+ 'entries' => reply[1]
137
+ }
138
+ }
139
+
140
+ HashifyStreamPendings = lambda { |reply|
141
+ {
142
+ 'size' => reply[0],
143
+ 'min_entry_id' => reply[1],
144
+ 'max_entry_id' => reply[2],
145
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
146
+ }
147
+ }
148
+
149
+ HashifyStreamPendingDetails = lambda { |reply|
150
+ reply.map do |arr|
151
+ {
152
+ 'entry_id' => arr[0],
153
+ 'consumer' => arr[1],
154
+ 'elapsed' => arr[2],
155
+ 'count' => arr[3]
156
+ }
157
+ end
158
+ }
159
+
160
+ HashifyClusterNodeInfo = lambda { |str|
161
+ arr = str.split(' ')
162
+ {
163
+ 'node_id' => arr[0],
164
+ 'ip_port' => arr[1],
165
+ 'flags' => arr[2].split(','),
166
+ 'master_node_id' => arr[3],
167
+ 'ping_sent' => arr[4],
168
+ 'pong_recv' => arr[5],
169
+ 'config_epoch' => arr[6],
170
+ 'link_state' => arr[7],
171
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
172
+ }
173
+ }
174
+
175
+ HashifyClusterSlots = lambda { |reply|
176
+ reply.map do |arr|
177
+ first_slot, last_slot = arr[0..1]
178
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
179
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
180
+ {
181
+ 'start_slot' => first_slot,
182
+ 'end_slot' => last_slot,
183
+ 'master' => master,
184
+ 'replicas' => replicas
185
+ }
186
+ end
187
+ }
188
+
189
+ HashifyClusterNodes = lambda { |reply|
190
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
191
+ }
192
+
193
+ HashifyClusterSlaves = lambda { |reply|
194
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
195
+ }
196
+
197
+ Noop = ->(reply) { reply }
198
+
199
+ # Sends a command to Redis and returns its reply.
200
+ #
201
+ # Replies are converted to Ruby objects according to the RESP protocol, so
202
+ # you can expect a Ruby array, integer or nil when Redis sends one. Higher
203
+ # level transformations, such as converting an array of pairs into a Ruby
204
+ # hash, are up to consumers.
205
+ #
206
+ # Redis error replies are raised as Ruby exceptions.
207
+ def call(*command)
208
+ send_command(command)
209
+ end
210
+
211
+ # Interact with the sentinel command (masters, master, slaves, failover)
212
+ #
213
+ # @param [String] subcommand e.g. `masters`, `master`, `slaves`
214
+ # @param [Array<String>] args depends on subcommand
215
+ # @return [Array<String>, Hash<String, String>, String] depends on subcommand
216
+ def sentinel(subcommand, *args)
217
+ subcommand = subcommand.to_s.downcase
218
+ send_command([:sentinel, subcommand] + args) do |reply|
219
+ case subcommand
220
+ when "get-master-addr-by-name"
221
+ reply
222
+ else
223
+ if reply.is_a?(Array)
224
+ if reply[0].is_a?(Array)
225
+ reply.map(&Hashify)
226
+ else
227
+ Hashify.call(reply)
228
+ end
229
+ else
230
+ reply
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ def method_missing(*command) # rubocop:disable Style/MissingRespondToMissing
239
+ send_command(command)
240
+ end
241
+ end
242
+ 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
 
@@ -14,8 +15,6 @@ class Redis
14
15
 
15
16
  if config[:scheme] == "unix"
16
17
  connection.connect_unix(config[:path], connect_timeout)
17
- elsif config[:scheme] == "rediss" || config[:ssl]
18
- raise NotImplementedError, "SSL not supported by hiredis driver"
19
18
  else
20
19
  connection.connect(config[:host], config[:port], connect_timeout)
21
20
  end
@@ -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)
@@ -32,30 +33,13 @@ class Redis
32
33
  @write_timeout = (timeout if timeout && timeout > 0)
33
34
  end
34
35
 
35
- string_capacity_support = begin
36
- String.new(capacity: 0)
37
- true # Ruby 2.4+
38
- rescue ArgumentError
39
- false # Ruby 2.3
40
- end
41
-
42
- if string_capacity_support
43
- def read(nbytes)
44
- result = @buffer.slice!(0, nbytes)
45
-
46
- buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
47
- result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
36
+ def read(nbytes)
37
+ result = @buffer.slice!(0, nbytes)
48
38
 
49
- result
50
- end
51
- else
52
- def read(nbytes)
53
- result = @buffer.slice!(0, nbytes)
39
+ buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
40
+ result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
54
41
 
55
- result << _read_from_socket(nbytes - result.bytesize, "".b) while result.bytesize < nbytes
56
-
57
- result
58
- end
42
+ result
59
43
  end
60
44
 
61
45
  def gets
@@ -150,7 +134,9 @@ class Redis
150
134
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
151
135
  # Use the blocking #readpartial method instead.
152
136
 
153
- 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
154
140
  readpartial(nbytes)
155
141
  rescue EOFError
156
142
  raise Errno::ECONNRESET
@@ -257,7 +243,7 @@ class Redis
257
243
  end
258
244
 
259
245
  def self.connect(host, port, timeout, ssl_params)
260
- # Note: this is using Redis::Connection::TCPSocket
246
+ # NOTE: this is using Redis::Connection::TCPSocket
261
247
  tcp_sock = TCPSocket.connect(host, port, timeout)
262
248
 
263
249
  ctx = OpenSSL::SSL::SSLContext.new
@@ -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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "hash_ring"
3
+ require "redis/hash_ring"
4
4
 
5
5
  class Redis
6
6
  class Distributed
@@ -178,15 +178,11 @@ class Redis
178
178
  # Determine if a key exists.
179
179
  def exists(*args)
180
180
  if !Redis.exists_returns_integer && args.size == 1
181
- message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \
181
+ ::Redis.deprecate!(
182
+ "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \
182
183
  "use `exists?` instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = true. " \
183
184
  "(#{::Kernel.caller(1, 1).first})\n"
184
-
185
- if defined?(::Warning)
186
- ::Warning.warn(message)
187
- else
188
- warn(message)
189
- end
185
+ )
190
186
  exists?(*args)
191
187
  else
192
188
  keys_per_node = args.group_by { |key| node_for(key) }
@@ -215,6 +211,13 @@ class Redis
215
211
  node_for(key).move(key, db)
216
212
  end
217
213
 
214
+ # Copy a value from one key to another.
215
+ def copy(source, destination, **options)
216
+ ensure_same_node(:copy, [source, destination]) do |node|
217
+ node.copy(source, destination, **options)
218
+ end
219
+ end
220
+
218
221
  # Return a random key from the keyspace.
219
222
  def randomkey
220
223
  raise CannotDistribute, :randomkey
@@ -666,11 +669,19 @@ class Redis
666
669
  node_for(key).zmscore(key, *members)
667
670
  end
668
671
 
669
- # Return a range of members in a sorted set, by index.
672
+ # Return a range of members in a sorted set, by index, score or lexicographical ordering.
670
673
  def zrange(key, start, stop, **options)
671
674
  node_for(key).zrange(key, start, stop, **options)
672
675
  end
673
676
 
677
+ # Select a range of members in a sorted set, by index, score or lexicographical ordering
678
+ # and store the resulting sorted set in a new key.
679
+ def zrangestore(dest_key, src_key, start, stop, **options)
680
+ ensure_same_node(:zrangestore, [dest_key, src_key]) do |node|
681
+ node.zrangestore(dest_key, src_key, start, stop, **options)
682
+ end
683
+ end
684
+
674
685
  # Return a range of members in a sorted set, by index, with scores ordered
675
686
  # from high to low.
676
687
  def zrevrange(key, start, stop, **options)
@@ -729,6 +740,13 @@ class Redis
729
740
  end
730
741
  end
731
742
 
743
+ # Return the union of multiple sorted sets.
744
+ def zunion(*keys, **options)
745
+ ensure_same_node(:zunion, keys) do |node|
746
+ node.zunion(*keys, **options)
747
+ end
748
+ end
749
+
732
750
  # Add multiple sorted sets and store the resulting sorted set in a new key.
733
751
  def zunionstore(destination, keys, **options)
734
752
  ensure_same_node(:zunionstore, [destination] + keys) do |node|
@@ -736,6 +754,21 @@ class Redis
736
754
  end
737
755
  end
738
756
 
757
+ # Return the difference between the first and all successive input sorted sets.
758
+ def zdiff(*keys, **options)
759
+ ensure_same_node(:zdiff, keys) do |node|
760
+ node.zdiff(*keys, **options)
761
+ end
762
+ end
763
+
764
+ # Compute the difference between the first and all successive input sorted sets
765
+ # and store the resulting sorted set in a new key.
766
+ def zdiffstore(destination, keys, **options)
767
+ ensure_same_node(:zdiffstore, [destination] + keys) do |node|
768
+ node.zdiffstore(destination, keys, **options)
769
+ end
770
+ end
771
+
739
772
  # Get the number of fields in a hash.
740
773
  def hlen(key)
741
774
  node_for(key).hlen(key)
@@ -774,6 +807,10 @@ class Redis
774
807
  Hash[*fields.zip(hmget(key, *fields)).flatten]
775
808
  end
776
809
 
810
+ def hrandfield(key, count = nil, **options)
811
+ node_for(key).hrandfield(key, count, **options)
812
+ end
813
+
777
814
  # Delete one or more hash fields.
778
815
  def hdel(key, *fields)
779
816
  node_for(key).hdel(key, *fields)
data/lib/redis/errors.rb CHANGED
@@ -45,6 +45,15 @@ class Redis
45
45
  end
46
46
 
47
47
  class Cluster
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
+
48
57
  # Raised when client connected to redis as cluster mode
49
58
  # and some cluster subcommands were called.
50
59
  class OrchestrationCommandNotSupported < BaseError