redis 4.5.1 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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