redis 4.5.1 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Transactions
6
+ # Watch the given keys to determine execution of the MULTI/EXEC block.
7
+ #
8
+ # Using a block is optional, but is necessary for thread-safety.
9
+ #
10
+ # An `#unwatch` is automatically issued if an exception is raised within the
11
+ # block that is a subclass of StandardError and is not a ConnectionError.
12
+ #
13
+ # @example With a block
14
+ # redis.watch("key") do
15
+ # if redis.get("key") == "some value"
16
+ # redis.multi do |multi|
17
+ # multi.set("key", "other value")
18
+ # multi.incr("counter")
19
+ # end
20
+ # else
21
+ # redis.unwatch
22
+ # end
23
+ # end
24
+ # # => ["OK", 6]
25
+ #
26
+ # @example Without a block
27
+ # redis.watch("key")
28
+ # # => "OK"
29
+ #
30
+ # @param [String, Array<String>] keys one or more keys to watch
31
+ # @return [Object] if using a block, returns the return value of the block
32
+ # @return [String] if not using a block, returns `OK`
33
+ #
34
+ # @see #unwatch
35
+ # @see #multi
36
+ def watch(*keys)
37
+ synchronize do |client|
38
+ res = client.call([:watch, *keys])
39
+
40
+ if block_given?
41
+ begin
42
+ yield(self)
43
+ rescue ConnectionError
44
+ raise
45
+ rescue StandardError
46
+ unwatch
47
+ raise
48
+ end
49
+ else
50
+ res
51
+ end
52
+ end
53
+ end
54
+
55
+ # Forget about all watched keys.
56
+ #
57
+ # @return [String] `OK`
58
+ #
59
+ # @see #watch
60
+ # @see #multi
61
+ def unwatch
62
+ send_command([:unwatch])
63
+ end
64
+
65
+ # Execute all commands issued after MULTI.
66
+ #
67
+ # Only call this method when `#multi` was called **without** a block.
68
+ #
69
+ # @return [nil, Array<...>]
70
+ # - when commands were not executed, `nil`
71
+ # - when commands were executed, an array with their replies
72
+ #
73
+ # @see #multi
74
+ # @see #discard
75
+ def exec
76
+ send_command([:exec])
77
+ end
78
+
79
+ # Discard all commands issued after MULTI.
80
+ #
81
+ # Only call this method when `#multi` was called **without** a block.
82
+ #
83
+ # @return [String] `"OK"`
84
+ #
85
+ # @see #multi
86
+ # @see #exec
87
+ def discard
88
+ send_command([:discard])
89
+ end
90
+ end
91
+ end
92
+ 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
 
@@ -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
@@ -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)
@@ -1,7 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+
3
5
  class Redis
6
+ class PipelinedConnection
7
+ def initialize(pipeline)
8
+ @pipeline = pipeline
9
+ end
10
+
11
+ include Commands
12
+
13
+ def db
14
+ @pipeline.db
15
+ end
16
+
17
+ def db=(db)
18
+ @pipeline.db = db
19
+ end
20
+
21
+ def pipelined
22
+ yield self
23
+ end
24
+
25
+ private
26
+
27
+ def synchronize
28
+ yield self
29
+ end
30
+
31
+ def send_command(command, &block)
32
+ @pipeline.call(command, &block)
33
+ end
34
+
35
+ def send_blocking_command(command, timeout, &block)
36
+ @pipeline.call_with_timeout(command, timeout, &block)
37
+ end
38
+ end
39
+
4
40
  class Pipeline
41
+ REDIS_INTERNAL_PATH = File.expand_path("..", __dir__).freeze
42
+ # Redis use MonitorMixin#synchronize and this class use DelegateClass which we want to filter out.
43
+ # Both are in the stdlib so we can simply filter the entire stdlib out.
44
+ STDLIB_PATH = File.expand_path("..", MonitorMixin.instance_method(:synchronize).source_location.first).freeze
45
+
46
+ class << self
47
+ def deprecation_warning(method, caller_locations) # :nodoc:
48
+ callsite = caller_locations.find { |l| !l.path.start_with?(REDIS_INTERNAL_PATH, STDLIB_PATH) }
49
+ callsite ||= caller_locations.last # The caller_locations should be large enough, but just in case.
50
+ ::Redis.deprecate! <<~MESSAGE
51
+ Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
52
+
53
+ redis.#{method} do
54
+ redis.get("key")
55
+ end
56
+
57
+ should be replaced by
58
+
59
+ redis.#{method} do |pipeline|
60
+ pipeline.get("key")
61
+ end
62
+
63
+ (called from #{callsite}}
64
+ MESSAGE
65
+ end
66
+ end
67
+
5
68
  attr_accessor :db
6
69
  attr_reader :client
7
70
 
@@ -124,6 +187,36 @@ class Redis
124
187
  end
125
188
  end
126
189
 
190
+ class DeprecatedPipeline < DelegateClass(Pipeline)
191
+ def initialize(pipeline)
192
+ super(pipeline)
193
+ @deprecation_displayed = false
194
+ end
195
+
196
+ def __getobj__
197
+ unless @deprecation_displayed
198
+ Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 10))
199
+ @deprecation_displayed = true
200
+ end
201
+ @delegate_dc_obj
202
+ end
203
+ end
204
+
205
+ class DeprecatedMulti < DelegateClass(Pipeline::Multi)
206
+ def initialize(pipeline)
207
+ super(pipeline)
208
+ @deprecation_displayed = false
209
+ end
210
+
211
+ def __getobj__
212
+ unless @deprecation_displayed
213
+ Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 10))
214
+ @deprecation_displayed = true
215
+ end
216
+ @delegate_dc_obj
217
+ end
218
+ end
219
+
127
220
  class FutureNotReady < RuntimeError
128
221
  def initialize
129
222
  super("Value will be available once the pipeline executes.")
@@ -143,11 +236,11 @@ class Redis
143
236
  end
144
237
 
145
238
  def ==(_other)
146
- message = +"The methods == and != are deprecated for Redis::Future and will be removed in 4.2.0"
239
+ message = +"The methods == and != are deprecated for Redis::Future and will be removed in 5.0.0"
147
240
  message << " - You probably meant to call .value == or .value !="
148
241
  message << " (#{::Kernel.caller(1, 1).first})\n"
149
242
 
150
- ::Kernel.warn(message)
243
+ ::Redis.deprecate!(message)
151
244
 
152
245
  super
153
246
  end
data/lib/redis/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Redis
4
- VERSION = '4.5.1'
4
+ VERSION = '4.6.0'
5
5
  end