redis 4.5.1 → 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.
@@ -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
@@ -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
 
@@ -133,7 +134,9 @@ class Redis
133
134
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
134
135
  # Use the blocking #readpartial method instead.
135
136
 
136
- 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
137
140
  readpartial(nbytes)
138
141
  rescue EOFError
139
142
  raise Errno::ECONNRESET
@@ -240,7 +243,7 @@ class Redis
240
243
  end
241
244
 
242
245
  def self.connect(host, port, timeout, ssl_params)
243
- # Note: this is using Redis::Connection::TCPSocket
246
+ # NOTE: this is using Redis::Connection::TCPSocket
244
247
  tcp_sock = TCPSocket.connect(host, port, timeout)
245
248
 
246
249
  ctx = OpenSSL::SSL::SSLContext.new
@@ -381,6 +384,12 @@ class Redis
381
384
  format_reply(reply_type, line)
382
385
  rescue Errno::EAGAIN
383
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
384
393
  end
385
394
 
386
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
@@ -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
@@ -115,13 +115,13 @@ class Redis
115
115
  end
116
116
 
117
117
  # Set a key's time to live in seconds.
118
- def expire(key, seconds)
119
- node_for(key).expire(key, seconds)
118
+ def expire(key, seconds, **kwargs)
119
+ node_for(key).expire(key, seconds, **kwargs)
120
120
  end
121
121
 
122
122
  # Set the expiration for a key as a UNIX timestamp.
123
- def expireat(key, unix_time)
124
- node_for(key).expireat(key, unix_time)
123
+ def expireat(key, unix_time, **kwargs)
124
+ node_for(key).expireat(key, unix_time, **kwargs)
125
125
  end
126
126
 
127
127
  # Get the time to live (in seconds) for a key.
@@ -130,13 +130,13 @@ class Redis
130
130
  end
131
131
 
132
132
  # Set a key's time to live in milliseconds.
133
- def pexpire(key, milliseconds)
134
- node_for(key).pexpire(key, milliseconds)
133
+ def pexpire(key, milliseconds, **kwarg)
134
+ node_for(key).pexpire(key, milliseconds, **kwarg)
135
135
  end
136
136
 
137
137
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
138
- def pexpireat(key, ms_unix_time)
139
- node_for(key).pexpireat(key, ms_unix_time)
138
+ def pexpireat(key, ms_unix_time, **kwarg)
139
+ node_for(key).pexpireat(key, ms_unix_time, **kwarg)
140
140
  end
141
141
 
142
142
  # Get the time to live (in milliseconds) for a key.
@@ -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
@@ -461,12 +464,13 @@ class Redis
461
464
  options = args.pop
462
465
  options[:timeout]
463
466
  elsif args.last.respond_to?(:to_int)
464
- # Issue deprecation notice in obnoxious mode...
465
- args.pop.to_int
466
- end
467
-
468
- if args.size > 1
469
- # Issue deprecation notice in obnoxious mode...
467
+ last_arg = args.pop
468
+ ::Redis.deprecate!(
469
+ "Passing the timeout as a positional argument is deprecated, it should be passed as a keyword argument:\n" \
470
+ " redis.#{cmd}(#{args.map(&:inspect).join(', ')}, timeout: #{last_arg.to_int})" \
471
+ "(called from: #{caller(2, 1).first})"
472
+ )
473
+ last_arg.to_int
470
474
  end
471
475
 
472
476
  keys = args.flatten
@@ -540,11 +544,21 @@ class Redis
540
544
  node_for(key).sadd(key, member)
541
545
  end
542
546
 
547
+ # Add one or more members to a set.
548
+ def sadd?(key, member)
549
+ node_for(key).sadd?(key, member)
550
+ end
551
+
543
552
  # Remove one or more members from a set.
544
553
  def srem(key, member)
545
554
  node_for(key).srem(key, member)
546
555
  end
547
556
 
557
+ # Remove one or more members from a set.
558
+ def srem?(key, member)
559
+ node_for(key).srem?(key, member)
560
+ end
561
+
548
562
  # Remove and return a random member from a set.
549
563
  def spop(key, count = nil)
550
564
  node_for(key).spop(key, count)
@@ -666,11 +680,19 @@ class Redis
666
680
  node_for(key).zmscore(key, *members)
667
681
  end
668
682
 
669
- # Return a range of members in a sorted set, by index.
683
+ # Return a range of members in a sorted set, by index, score or lexicographical ordering.
670
684
  def zrange(key, start, stop, **options)
671
685
  node_for(key).zrange(key, start, stop, **options)
672
686
  end
673
687
 
688
+ # Select a range of members in a sorted set, by index, score or lexicographical ordering
689
+ # and store the resulting sorted set in a new key.
690
+ def zrangestore(dest_key, src_key, start, stop, **options)
691
+ ensure_same_node(:zrangestore, [dest_key, src_key]) do |node|
692
+ node.zrangestore(dest_key, src_key, start, stop, **options)
693
+ end
694
+ end
695
+
674
696
  # Return a range of members in a sorted set, by index, with scores ordered
675
697
  # from high to low.
676
698
  def zrevrange(key, start, stop, **options)
@@ -729,6 +751,13 @@ class Redis
729
751
  end
730
752
  end
731
753
 
754
+ # Return the union of multiple sorted sets.
755
+ def zunion(*keys, **options)
756
+ ensure_same_node(:zunion, keys) do |node|
757
+ node.zunion(*keys, **options)
758
+ end
759
+ end
760
+
732
761
  # Add multiple sorted sets and store the resulting sorted set in a new key.
733
762
  def zunionstore(destination, keys, **options)
734
763
  ensure_same_node(:zunionstore, [destination] + keys) do |node|
@@ -736,6 +765,21 @@ class Redis
736
765
  end
737
766
  end
738
767
 
768
+ # Return the difference between the first and all successive input sorted sets.
769
+ def zdiff(*keys, **options)
770
+ ensure_same_node(:zdiff, keys) do |node|
771
+ node.zdiff(*keys, **options)
772
+ end
773
+ end
774
+
775
+ # Compute the difference between the first and all successive input sorted sets
776
+ # and store the resulting sorted set in a new key.
777
+ def zdiffstore(destination, keys, **options)
778
+ ensure_same_node(:zdiffstore, [destination] + keys) do |node|
779
+ node.zdiffstore(destination, keys, **options)
780
+ end
781
+ end
782
+
739
783
  # Get the number of fields in a hash.
740
784
  def hlen(key)
741
785
  node_for(key).hlen(key)
@@ -774,6 +818,10 @@ class Redis
774
818
  Hash[*fields.zip(hmget(key, *fields)).flatten]
775
819
  end
776
820
 
821
+ def hrandfield(key, count = nil, **options)
822
+ node_for(key).hrandfield(key, count, **options)
823
+ end
824
+
777
825
  # Delete one or more hash fields.
778
826
  def hdel(key, *fields)
779
827
  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