redis 4.1.4 → 4.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +141 -0
- data/README.md +52 -27
- data/lib/redis/client.rb +122 -87
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +8 -9
- data/lib/redis/cluster/node.rb +17 -1
- data/lib/redis/cluster/node_loader.rb +8 -11
- data/lib/redis/cluster/option.rb +18 -5
- data/lib/redis/cluster/slot.rb +28 -14
- data/lib/redis/cluster/slot_loader.rb +11 -15
- data/lib/redis/cluster.rb +37 -13
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +411 -0
- data/lib/redis/commands/lists.rb +289 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +207 -0
- data/lib/redis/commands/sorted_sets.rb +812 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +139 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +4 -2
- data/lib/redis/connection/hiredis.rb +6 -7
- data/lib/redis/connection/registry.rb +1 -1
- data/lib/redis/connection/ruby.rb +106 -114
- data/lib/redis/connection/synchrony.rb +16 -10
- data/lib/redis/connection.rb +2 -1
- data/lib/redis/distributed.rb +200 -65
- data/lib/redis/errors.rb +10 -0
- data/lib/redis/hash_ring.rb +14 -14
- data/lib/redis/pipeline.rb +133 -10
- data/lib/redis/subscribe.rb +10 -12
- data/lib/redis/version.rb +2 -1
- data/lib/redis.rb +158 -3358
- metadata +32 -10
data/lib/redis/cluster/slot.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'set'
|
4
|
-
|
5
3
|
class Redis
|
6
4
|
class Cluster
|
7
5
|
# Keep slot and node key map for Redis Cluster Client
|
@@ -28,11 +26,20 @@ class Redis
|
|
28
26
|
return nil unless exists?(slot)
|
29
27
|
return find_node_key_of_master(slot) if replica_disabled?
|
30
28
|
|
31
|
-
@map[slot][:slaves].
|
29
|
+
@map[slot][:slaves].sample
|
32
30
|
end
|
33
31
|
|
34
32
|
def put(slot, node_key)
|
35
|
-
|
33
|
+
# Since we're sharing a hash for build_slot_node_key_map, duplicate it
|
34
|
+
# if it already exists instead of preserving as-is.
|
35
|
+
@map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
|
36
|
+
|
37
|
+
if master?(node_key)
|
38
|
+
@map[slot][:master] = node_key
|
39
|
+
elsif !@map[slot][:slaves].include?(node_key)
|
40
|
+
@map[slot][:slaves] << node_key
|
41
|
+
end
|
42
|
+
|
36
43
|
nil
|
37
44
|
end
|
38
45
|
|
@@ -52,20 +59,27 @@ class Redis
|
|
52
59
|
|
53
60
|
# available_slots is mapping of node_key to list of slot ranges
|
54
61
|
def build_slot_node_key_map(available_slots)
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
by_ranges = {}
|
63
|
+
available_slots.each do |node_key, slots_arr|
|
64
|
+
by_ranges[slots_arr] ||= { master: nil, slaves: [] }
|
65
|
+
|
66
|
+
if master?(node_key)
|
67
|
+
by_ranges[slots_arr][:master] = node_key
|
68
|
+
elsif !by_ranges[slots_arr][:slaves].include?(node_key)
|
69
|
+
by_ranges[slots_arr][:slaves] << node_key
|
58
70
|
end
|
59
71
|
end
|
60
|
-
end
|
61
72
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
73
|
+
by_slot = {}
|
74
|
+
by_ranges.each do |slots_arr, nodes|
|
75
|
+
slots_arr.each do |slots|
|
76
|
+
slots.each do |slot|
|
77
|
+
by_slot[slot] = nodes
|
78
|
+
end
|
79
|
+
end
|
68
80
|
end
|
81
|
+
|
82
|
+
by_slot
|
69
83
|
end
|
70
84
|
end
|
71
85
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'redis/errors'
|
4
|
+
require 'redis/cluster/node_key'
|
5
5
|
|
6
6
|
class Redis
|
7
7
|
class Cluster
|
@@ -10,26 +10,22 @@ class Redis
|
|
10
10
|
module_function
|
11
11
|
|
12
12
|
def load(nodes)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
errors = nodes.map do |node|
|
14
|
+
begin
|
15
|
+
return fetch_slot_info(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
21
|
+
raise InitialSetupError, errors
|
23
22
|
end
|
24
23
|
|
25
24
|
def fetch_slot_info(node)
|
26
25
|
hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
|
27
26
|
node.call(%i[cluster slots])
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
32
|
-
{} # can retry on another node
|
27
|
+
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
28
|
+
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
33
29
|
end
|
34
30
|
|
35
31
|
def parse_slot_info(arr, default_ip:)
|
data/lib/redis/cluster.rb
CHANGED
@@ -78,10 +78,13 @@ class Redis
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def call_pipeline(pipeline)
|
81
|
-
node_keys
|
82
|
-
|
83
|
-
|
84
|
-
|
81
|
+
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
|
82
|
+
if node_keys.size > 1
|
83
|
+
raise(CrossSlotPipeliningError,
|
84
|
+
pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
|
85
|
+
end
|
86
|
+
|
87
|
+
try_send(find_node(node_keys.first), :call_pipeline, pipeline)
|
85
88
|
end
|
86
89
|
|
87
90
|
def call_with_timeout(command, timeout, &block)
|
@@ -127,13 +130,14 @@ class Redis
|
|
127
130
|
def send_command(command, &block)
|
128
131
|
cmd = command.first.to_s.downcase
|
129
132
|
case cmd
|
130
|
-
when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
133
|
+
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
131
134
|
@node.call_all(command, &block).first
|
132
135
|
when 'flushall', 'flushdb'
|
133
136
|
@node.call_master(command, &block).first
|
134
137
|
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
135
138
|
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
136
139
|
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
140
|
+
when 'scan' then _scan(command, &block)
|
137
141
|
when 'lastsave' then @node.call_all(command, &block).sort
|
138
142
|
when 'role' then @node.call_all(command, &block)
|
139
143
|
when 'config' then send_config_command(command, &block)
|
@@ -216,11 +220,13 @@ class Redis
|
|
216
220
|
rescue CommandError => err
|
217
221
|
if err.message.start_with?('MOVED')
|
218
222
|
raise if retry_count <= 0
|
223
|
+
|
219
224
|
node = assign_redirection_node(err.message)
|
220
225
|
retry_count -= 1
|
221
226
|
retry
|
222
227
|
elsif err.message.start_with?('ASK')
|
223
228
|
raise if retry_count <= 0
|
229
|
+
|
224
230
|
node = assign_asking_node(err.message)
|
225
231
|
node.call(%i[asking])
|
226
232
|
retry_count -= 1
|
@@ -233,6 +239,29 @@ class Redis
|
|
233
239
|
raise
|
234
240
|
end
|
235
241
|
|
242
|
+
def _scan(command, &block)
|
243
|
+
input_cursor = Integer(command[1])
|
244
|
+
|
245
|
+
client_index = input_cursor % 256
|
246
|
+
raw_cursor = input_cursor >> 8
|
247
|
+
|
248
|
+
clients = @node.scale_reading_clients
|
249
|
+
|
250
|
+
client = clients[client_index]
|
251
|
+
return ['0', []] unless client
|
252
|
+
|
253
|
+
command[1] = raw_cursor.to_s
|
254
|
+
|
255
|
+
result_cursor, result_keys = client.call(command, &block)
|
256
|
+
result_cursor = Integer(result_cursor)
|
257
|
+
|
258
|
+
if result_cursor == 0
|
259
|
+
client_index += 1
|
260
|
+
end
|
261
|
+
|
262
|
+
[((result_cursor << 8) + client_index).to_s, result_keys]
|
263
|
+
end
|
264
|
+
|
236
265
|
def assign_redirection_node(err_msg)
|
237
266
|
_, slot, node_key = err_msg.split(' ')
|
238
267
|
slot = slot.to_i
|
@@ -250,14 +279,14 @@ class Redis
|
|
250
279
|
find_node(node_key)
|
251
280
|
end
|
252
281
|
|
253
|
-
def find_node_key(command)
|
282
|
+
def find_node_key(command, primary_only: false)
|
254
283
|
key = @command.extract_first_key(command)
|
255
284
|
return if key.empty?
|
256
285
|
|
257
286
|
slot = KeySlotConverter.convert(key)
|
258
287
|
return unless @slot.exists?(slot)
|
259
288
|
|
260
|
-
if @command.should_send_to_master?(command)
|
289
|
+
if @command.should_send_to_master?(command) || primary_only
|
261
290
|
@slot.find_node_key_of_master(slot)
|
262
291
|
else
|
263
292
|
@slot.find_node_key_of_slave(slot)
|
@@ -266,6 +295,7 @@ class Redis
|
|
266
295
|
|
267
296
|
def find_node(node_key)
|
268
297
|
return @node.sample if node_key.nil?
|
298
|
+
|
269
299
|
@node.find_by(node_key)
|
270
300
|
rescue Node::ReloadNeeded
|
271
301
|
update_cluster_info!(node_key)
|
@@ -281,11 +311,5 @@ class Redis
|
|
281
311
|
@node.map(&:disconnect)
|
282
312
|
@node, @slot = fetch_cluster_info!(@option)
|
283
313
|
end
|
284
|
-
|
285
|
-
def extract_keys_in_pipeline(pipeline)
|
286
|
-
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
|
287
|
-
command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
|
288
|
-
[node_keys, command_keys]
|
289
|
-
end
|
290
314
|
end
|
291
315
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Bitmaps
|
6
|
+
# Sets or clears the bit at offset in the string value stored at key.
|
7
|
+
#
|
8
|
+
# @param [String] key
|
9
|
+
# @param [Integer] offset bit offset
|
10
|
+
# @param [Integer] value bit value `0` or `1`
|
11
|
+
# @return [Integer] the original bit value stored at `offset`
|
12
|
+
def setbit(key, offset, value)
|
13
|
+
send_command([:setbit, key, offset, value])
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the bit value at offset in the string value stored at key.
|
17
|
+
#
|
18
|
+
# @param [String] key
|
19
|
+
# @param [Integer] offset bit offset
|
20
|
+
# @return [Integer] `0` or `1`
|
21
|
+
def getbit(key, offset)
|
22
|
+
send_command([:getbit, key, offset])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Count the number of set bits in a range of the string value stored at key.
|
26
|
+
#
|
27
|
+
# @param [String] key
|
28
|
+
# @param [Integer] start start index
|
29
|
+
# @param [Integer] stop stop index
|
30
|
+
# @return [Integer] the number of bits set to 1
|
31
|
+
def bitcount(key, start = 0, stop = -1)
|
32
|
+
send_command([:bitcount, key, start, stop])
|
33
|
+
end
|
34
|
+
|
35
|
+
# Perform a bitwise operation between strings and store the resulting string in a key.
|
36
|
+
#
|
37
|
+
# @param [String] operation e.g. `and`, `or`, `xor`, `not`
|
38
|
+
# @param [String] destkey destination key
|
39
|
+
# @param [String, Array<String>] keys one or more source keys to perform `operation`
|
40
|
+
# @return [Integer] the length of the string stored in `destkey`
|
41
|
+
def bitop(operation, destkey, *keys)
|
42
|
+
send_command([:bitop, operation, destkey, *keys])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the position of the first bit set to 1 or 0 in a string.
|
46
|
+
#
|
47
|
+
# @param [String] key
|
48
|
+
# @param [Integer] bit whether to look for the first 1 or 0 bit
|
49
|
+
# @param [Integer] start start index
|
50
|
+
# @param [Integer] stop stop index
|
51
|
+
# @return [Integer] the position of the first 1/0 bit.
|
52
|
+
# -1 if looking for 1 and it is not found or start and stop are given.
|
53
|
+
def bitpos(key, bit, start = nil, stop = nil)
|
54
|
+
raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
|
55
|
+
|
56
|
+
command = [:bitpos, key, bit]
|
57
|
+
command << start if start
|
58
|
+
command << stop if stop
|
59
|
+
send_command(command)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Cluster
|
6
|
+
# Sends `CLUSTER *` command to random node and returns its reply.
|
7
|
+
#
|
8
|
+
# @see https://redis.io/commands#cluster Reference of cluster command
|
9
|
+
#
|
10
|
+
# @param subcommand [String, Symbol] the subcommand of cluster command
|
11
|
+
# e.g. `:slots`, `:nodes`, `:slaves`, `:info`
|
12
|
+
#
|
13
|
+
# @return [Object] depends on the subcommand
|
14
|
+
def cluster(subcommand, *args)
|
15
|
+
subcommand = subcommand.to_s.downcase
|
16
|
+
block = case subcommand
|
17
|
+
when 'slots'
|
18
|
+
HashifyClusterSlots
|
19
|
+
when 'nodes'
|
20
|
+
HashifyClusterNodes
|
21
|
+
when 'slaves'
|
22
|
+
HashifyClusterSlaves
|
23
|
+
when 'info'
|
24
|
+
HashifyInfo
|
25
|
+
else
|
26
|
+
Noop
|
27
|
+
end
|
28
|
+
|
29
|
+
# @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
|
30
|
+
block = Noop unless @cluster_mode
|
31
|
+
|
32
|
+
send_command([:cluster, subcommand] + args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sends `ASKING` command to random node and returns its reply.
|
36
|
+
#
|
37
|
+
# @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
|
38
|
+
#
|
39
|
+
# @return [String] `'OK'`
|
40
|
+
def asking
|
41
|
+
send_command(%i[asking])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Connection
|
6
|
+
# Authenticate to the server.
|
7
|
+
#
|
8
|
+
# @param [Array<String>] args includes both username and password
|
9
|
+
# or only password
|
10
|
+
# @return [String] `OK`
|
11
|
+
# @see https://redis.io/commands/auth AUTH command
|
12
|
+
def auth(*args)
|
13
|
+
send_command([:auth, *args])
|
14
|
+
end
|
15
|
+
|
16
|
+
# Ping the server.
|
17
|
+
#
|
18
|
+
# @param [optional, String] message
|
19
|
+
# @return [String] `PONG`
|
20
|
+
def ping(message = nil)
|
21
|
+
send_command([:ping, message].compact)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Echo the given string.
|
25
|
+
#
|
26
|
+
# @param [String] value
|
27
|
+
# @return [String]
|
28
|
+
def echo(value)
|
29
|
+
send_command([:echo, value])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Change the selected database for the current connection.
|
33
|
+
#
|
34
|
+
# @param [Integer] db zero-based index of the DB to use (0 to 15)
|
35
|
+
# @return [String] `OK`
|
36
|
+
def select(db)
|
37
|
+
synchronize do |client|
|
38
|
+
client.db = db
|
39
|
+
client.call([:select, db])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Close the connection.
|
44
|
+
#
|
45
|
+
# @return [String] `OK`
|
46
|
+
def quit
|
47
|
+
synchronize do |client|
|
48
|
+
begin
|
49
|
+
client.call([:quit])
|
50
|
+
rescue ConnectionError
|
51
|
+
ensure
|
52
|
+
client.disconnect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Geo
|
6
|
+
# Adds the specified geospatial items (latitude, longitude, name) to the specified key
|
7
|
+
#
|
8
|
+
# @param [String] key
|
9
|
+
# @param [Array] member arguemnts for member or members: longitude, latitude, name
|
10
|
+
# @return [Integer] number of elements added to the sorted set
|
11
|
+
def geoadd(key, *member)
|
12
|
+
send_command([:geoadd, key, *member])
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns geohash string representing position for specified members of the specified key.
|
16
|
+
#
|
17
|
+
# @param [String] key
|
18
|
+
# @param [String, Array<String>] member one member or array of members
|
19
|
+
# @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
|
20
|
+
def geohash(key, member)
|
21
|
+
send_command([:geohash, key, member])
|
22
|
+
end
|
23
|
+
|
24
|
+
# Query a sorted set representing a geospatial index to fetch members matching a
|
25
|
+
# given maximum distance from a point
|
26
|
+
#
|
27
|
+
# @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
|
28
|
+
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
|
29
|
+
# or the farthest to the nearest relative to the center
|
30
|
+
# @param [Integer] count limit the results to the first N matching items
|
31
|
+
# @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
|
32
|
+
# @return [Array<String>] may be changed with `options`
|
33
|
+
def georadius(*args, **geoptions)
|
34
|
+
geoarguments = _geoarguments(*args, **geoptions)
|
35
|
+
|
36
|
+
send_command([:georadius, *geoarguments])
|
37
|
+
end
|
38
|
+
|
39
|
+
# Query a sorted set representing a geospatial index to fetch members matching a
|
40
|
+
# given maximum distance from an already existing member
|
41
|
+
#
|
42
|
+
# @param [Array] args key, member, radius, unit(m|km|ft|mi)
|
43
|
+
# @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
|
44
|
+
# to the nearest relative to the center
|
45
|
+
# @param [Integer] count limit the results to the first N matching items
|
46
|
+
# @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
|
47
|
+
# @return [Array<String>] may be changed with `options`
|
48
|
+
def georadiusbymember(*args, **geoptions)
|
49
|
+
geoarguments = _geoarguments(*args, **geoptions)
|
50
|
+
|
51
|
+
send_command([:georadiusbymember, *geoarguments])
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns longitude and latitude of members of a geospatial index
|
55
|
+
#
|
56
|
+
# @param [String] key
|
57
|
+
# @param [String, Array<String>] member one member or array of members
|
58
|
+
# @return [Array<Array<String>, nil>] returns array of elements, where each
|
59
|
+
# element is either array of longitude and latitude or nil
|
60
|
+
def geopos(key, member)
|
61
|
+
send_command([:geopos, key, member])
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the distance between two members of a geospatial index
|
65
|
+
#
|
66
|
+
# @param [String ]key
|
67
|
+
# @param [Array<String>] members
|
68
|
+
# @param ['m', 'km', 'mi', 'ft'] unit
|
69
|
+
# @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
|
70
|
+
def geodist(key, member1, member2, unit = 'm')
|
71
|
+
send_command([:geodist, key, member1, member2, unit])
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def _geoarguments(*args, options: nil, sort: nil, count: nil)
|
77
|
+
args.push sort if sort
|
78
|
+
args.push 'count', count if count
|
79
|
+
args.push options if options
|
80
|
+
args
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|