redis-cluster-client 0.16.6 → 0.17.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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7df1ba6006c9d3c3e7c242e9406f6443a95aa371c7e77244b73565058f476435
|
|
4
|
+
data.tar.gz: 5613bf92dc543482c1bcb2e74d61096092c299ef84877b8c06da1174db82c6d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58e9ecaa1aecf6bfb298cba78e08d349f742439a2f5a53e19cd55bb49c8a600b4e4f47a54af1798ef0afe51cb56cf5cf292eda3bdf75e522ed180fa065e37954
|
|
7
|
+
data.tar.gz: c158336f83f4453e27798a2521c4e282a867db446dfa7e74e988af9e568567235b321b59386bf79ac5d0e32dee0698137755e9c416ceeb3acb150ebfc60d5208
|
|
@@ -9,12 +9,11 @@ class RedisClient
|
|
|
9
9
|
class Command
|
|
10
10
|
EMPTY_STRING = ''
|
|
11
11
|
EMPTY_HASH = {}.freeze
|
|
12
|
-
EMPTY_POLICIES = [nil, nil].freeze
|
|
13
12
|
REQUEST_POLICY_PREFIX = 'request_policy:'
|
|
14
13
|
RESPONSE_POLICY_PREFIX = 'response_policy:'
|
|
15
14
|
SUBCOMMAND_DELIMITER = '|'
|
|
16
15
|
|
|
17
|
-
private_constant :EMPTY_HASH, :
|
|
16
|
+
private_constant :EMPTY_HASH, :REQUEST_POLICY_PREFIX,
|
|
18
17
|
:RESPONSE_POLICY_PREFIX, :SUBCOMMAND_DELIMITER
|
|
19
18
|
|
|
20
19
|
# @see https://redis.io/docs/latest/commands/command/ The reply of the COMMAND command
|
|
@@ -105,15 +104,13 @@ class RedisClient
|
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
def build_spec(row)
|
|
108
|
-
request_policy, response_policy = parse_tips(row[7])
|
|
109
|
-
|
|
110
107
|
::RedisClient::Cluster::Command::Spec.new(
|
|
111
108
|
first_key_position: parse_first_key_position(row),
|
|
112
109
|
key_step: row[5],
|
|
113
110
|
write?: parse_writability(row),
|
|
114
111
|
readonly?: row[2].include?('readonly'),
|
|
115
|
-
request_policy:
|
|
116
|
-
response_policy:
|
|
112
|
+
request_policy: parse_policy_tip(row[7], REQUEST_POLICY_PREFIX),
|
|
113
|
+
response_policy: parse_policy_tip(row[7], RESPONSE_POLICY_PREFIX),
|
|
117
114
|
subcommands: parse_subcommands(row[9])
|
|
118
115
|
).freeze
|
|
119
116
|
end
|
|
@@ -137,20 +134,10 @@ class RedisClient
|
|
|
137
134
|
end
|
|
138
135
|
|
|
139
136
|
# The command tips are available in the redis 7.0 or later.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
tips.each do |tip|
|
|
146
|
-
if tip.start_with?(REQUEST_POLICY_PREFIX)
|
|
147
|
-
request_policy = -tip[REQUEST_POLICY_PREFIX.size..]
|
|
148
|
-
elsif tip.start_with?(RESPONSE_POLICY_PREFIX)
|
|
149
|
-
response_policy = -tip[RESPONSE_POLICY_PREFIX.size..]
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
[request_policy, response_policy]
|
|
137
|
+
# It returns a single value instead of a pair to avoid the allocation of the array.
|
|
138
|
+
def parse_policy_tip(tips, prefix)
|
|
139
|
+
tip = tips&.find { |t| t.start_with?(prefix) }
|
|
140
|
+
tip.nil? ? nil : -tip.delete_prefix(prefix)
|
|
154
141
|
end
|
|
155
142
|
|
|
156
143
|
# The information of the subcommands is available in the redis 7.0 or later.
|
|
@@ -164,7 +151,7 @@ class RedisClient
|
|
|
164
151
|
|
|
165
152
|
# The server replies with a full name of the subcommand such as `xinfo|stream`.
|
|
166
153
|
i = name.index(SUBCOMMAND_DELIMITER)
|
|
167
|
-
acc[i.nil? ? name : name[
|
|
154
|
+
acc[i.nil? ? name : name[i + 1, name.size]] = build_spec(row)
|
|
168
155
|
end.freeze
|
|
169
156
|
end
|
|
170
157
|
end
|
|
@@ -26,7 +26,7 @@ class RedisClient
|
|
|
26
26
|
pos = node_key.rindex(DELIMITER, -1)
|
|
27
27
|
return [node_key, nil] if pos.nil?
|
|
28
28
|
|
|
29
|
-
[node_key[0, pos], node_key[
|
|
29
|
+
[node_key[0, pos], node_key[pos + 1, node_key.size]]
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def split_bracketed(node_key)
|
|
@@ -36,7 +36,7 @@ class RedisClient
|
|
|
36
36
|
return nil if end_bracket.nil?
|
|
37
37
|
|
|
38
38
|
host = node_key[1, end_bracket - 1]
|
|
39
|
-
remainder = node_key[
|
|
39
|
+
remainder = node_key[end_bracket + 1, node_key.size]
|
|
40
40
|
port = remainder.start_with?(DELIMITER) ? remainder[1..] : nil
|
|
41
41
|
[host, port]
|
|
42
42
|
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'redis_client/cluster/errors'
|
|
4
|
+
|
|
5
|
+
class RedisClient
|
|
6
|
+
class Cluster
|
|
7
|
+
class Router
|
|
8
|
+
# The routing table for the commands which shouldn't be routed by their keys.
|
|
9
|
+
# The built-in entries can be overridden with the `command_routings` option of the config.
|
|
10
|
+
class RoutingTable
|
|
11
|
+
BuildError = Class.new(::RedisClient::Cluster::Error)
|
|
12
|
+
RoutingAction = Struct.new('RedisCommandRoutingAction', :method_name, :reply_transformer, keyword_init: true)
|
|
13
|
+
PICK_FIRST = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
|
|
14
|
+
FLATTEN_STRINGS = ->(reply) { reply.flatten.sort_by(&:to_s) }
|
|
15
|
+
SUM_NUM = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
|
|
16
|
+
SORT_NUMBERS = ->(reply) { reply.sort_by(&:to_i) }
|
|
17
|
+
if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
|
|
18
|
+
Ractor.make_shareable(PICK_FIRST)
|
|
19
|
+
Ractor.make_shareable(FLATTEN_STRINGS)
|
|
20
|
+
Ractor.make_shareable(SUM_NUM)
|
|
21
|
+
Ractor.make_shareable(SORT_NUMBERS)
|
|
22
|
+
end
|
|
23
|
+
DEDICATED_ACTIONS = lambda do # rubocop:disable Metrics/BlockLength
|
|
24
|
+
multiple_key_action = RoutingAction.new(method_name: :send_multiple_keys_command)
|
|
25
|
+
all_node_first_action = RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST)
|
|
26
|
+
primary_first_action = RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST)
|
|
27
|
+
not_supported_action = RoutingAction.new(method_name: :fail_not_supported_command)
|
|
28
|
+
keyless_action = RoutingAction.new(method_name: :fail_keyless_command)
|
|
29
|
+
single_node_action = RoutingAction.new(method_name: :assign_node_and_send_command)
|
|
30
|
+
{
|
|
31
|
+
'ping' => RoutingAction.new(method_name: :send_ping_command, reply_transformer: PICK_FIRST),
|
|
32
|
+
'wait' => RoutingAction.new(method_name: :send_wait_command),
|
|
33
|
+
'keys' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: FLATTEN_STRINGS),
|
|
34
|
+
'dbsize' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: SUM_NUM),
|
|
35
|
+
'scan' => RoutingAction.new(method_name: :send_scan_command),
|
|
36
|
+
'lastsave' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SORT_NUMBERS),
|
|
37
|
+
'role' => RoutingAction.new(method_name: :send_command_to_all_nodes),
|
|
38
|
+
'config' => RoutingAction.new(method_name: :send_config_command),
|
|
39
|
+
'client' => RoutingAction.new(method_name: :send_client_command),
|
|
40
|
+
'cluster' => RoutingAction.new(method_name: :send_cluster_command),
|
|
41
|
+
'memory' => RoutingAction.new(method_name: :send_memory_command),
|
|
42
|
+
'script' => RoutingAction.new(method_name: :send_script_command),
|
|
43
|
+
'pubsub' => RoutingAction.new(method_name: :send_pubsub_command),
|
|
44
|
+
'watch' => RoutingAction.new(method_name: :send_watch_command),
|
|
45
|
+
'mget' => multiple_key_action,
|
|
46
|
+
'mset' => multiple_key_action,
|
|
47
|
+
'del' => multiple_key_action,
|
|
48
|
+
'acl' => all_node_first_action,
|
|
49
|
+
'auth' => all_node_first_action,
|
|
50
|
+
'bgrewriteaof' => all_node_first_action,
|
|
51
|
+
'bgsave' => all_node_first_action,
|
|
52
|
+
'quit' => all_node_first_action,
|
|
53
|
+
'save' => all_node_first_action,
|
|
54
|
+
'select' => all_node_first_action,
|
|
55
|
+
'flushall' => primary_first_action,
|
|
56
|
+
'flushdb' => primary_first_action,
|
|
57
|
+
# The redis 7.0 tags RANDOMKEY with `request_policy:all_shards` but without any response policy.
|
|
58
|
+
# It was corrected to `response_policy:special` in the redis 7.2.
|
|
59
|
+
# This entry keeps the historical single node routing for the redis 7.0.
|
|
60
|
+
'randomkey' => single_node_action,
|
|
61
|
+
'readonly' => not_supported_action,
|
|
62
|
+
'readwrite' => not_supported_action,
|
|
63
|
+
'shutdown' => not_supported_action,
|
|
64
|
+
'discard' => keyless_action,
|
|
65
|
+
'exec' => keyless_action,
|
|
66
|
+
'multi' => keyless_action,
|
|
67
|
+
'unwatch' => keyless_action
|
|
68
|
+
}.each_with_object({}) do |(k, v), acc|
|
|
69
|
+
acc[k] = v.freeze
|
|
70
|
+
acc[k.upcase] = v.freeze
|
|
71
|
+
end
|
|
72
|
+
end.call.freeze
|
|
73
|
+
|
|
74
|
+
# The routing which the command tips of the COMMAND command reply instruct.
|
|
75
|
+
# The `multi_shard` and the `special` request policies are out of scope.
|
|
76
|
+
# The `special` response policy is also out of scope because the aggregation is undefined,
|
|
77
|
+
# and the fan-out would change the return value of commands such as INFO.
|
|
78
|
+
# The `agg_min`, `agg_max` and `agg_logical_*` response policies are out of scope too:
|
|
79
|
+
# the only reachable command with them today is WAITAOF, whose reply is an array,
|
|
80
|
+
# and the aggregation of array replies is undefined. Such a command falls back to
|
|
81
|
+
# the single node routing until a command with a settled semantics appears.
|
|
82
|
+
# The entries of the DEDICATED_ACTIONS take precedence over these to keep the existing behavior.
|
|
83
|
+
# @see https://redis.io/docs/latest/develop/reference/command-tips/
|
|
84
|
+
POLICY_ACTIONS = {
|
|
85
|
+
'all_shards' => {
|
|
86
|
+
nil => RoutingAction.new(method_name: :send_command_to_primaries).freeze,
|
|
87
|
+
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST).freeze,
|
|
88
|
+
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
89
|
+
'agg_sum' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: SUM_NUM).freeze
|
|
90
|
+
}.freeze,
|
|
91
|
+
'all_nodes' => {
|
|
92
|
+
nil => RoutingAction.new(method_name: :send_command_to_all_nodes).freeze,
|
|
93
|
+
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST).freeze,
|
|
94
|
+
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
95
|
+
'agg_sum' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SUM_NUM).freeze
|
|
96
|
+
}.freeze
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
private_constant :RoutingAction, :PICK_FIRST, :FLATTEN_STRINGS, :SUM_NUM, :SORT_NUMBERS,
|
|
100
|
+
:DEDICATED_ACTIONS, :POLICY_ACTIONS
|
|
101
|
+
|
|
102
|
+
class << self
|
|
103
|
+
# Builds the routing table: the built-in entries overridden by the command routings
|
|
104
|
+
# which the config validated and normalized. A nil routing removes the built-in entry
|
|
105
|
+
# of the command, so that the command follows the default resolution: the command tips
|
|
106
|
+
# which the server reports, or the routing by its key.
|
|
107
|
+
# It raises a BuildError for an unnormalized input as a defense, e.g. an unsupported policy.
|
|
108
|
+
def build(routings)
|
|
109
|
+
return DEDICATED_ACTIONS if routings.nil? || routings.empty?
|
|
110
|
+
|
|
111
|
+
routings.each_with_object(DEDICATED_ACTIONS.dup) do |(key, policies), acc|
|
|
112
|
+
merge_action(acc, key, fetch_action(key, policies))
|
|
113
|
+
end.freeze
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def find_policy_action(request_policy, response_policy)
|
|
117
|
+
POLICY_ACTIONS.dig(request_policy, response_policy)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Stores both the lowercase and the uppercase keys to avoid a per-call case conversion.
|
|
123
|
+
def merge_action(table, key, action)
|
|
124
|
+
if action.nil?
|
|
125
|
+
table.delete(key)
|
|
126
|
+
table.delete(key.upcase)
|
|
127
|
+
else
|
|
128
|
+
table[key] = action
|
|
129
|
+
table[key.upcase] = action
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def fetch_action(key, policies)
|
|
134
|
+
return if policies.nil?
|
|
135
|
+
|
|
136
|
+
action = POLICY_ACTIONS.dig(policies[:request_policy], policies[:response_policy])
|
|
137
|
+
return action unless action.nil?
|
|
138
|
+
|
|
139
|
+
raise BuildError, "the policies of the #{key} command are unsupported: #{policies.inspect}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -11,106 +11,20 @@ require 'redis_client/cluster/transaction'
|
|
|
11
11
|
require 'redis_client/cluster/optimistic_locking'
|
|
12
12
|
require 'redis_client/cluster/pipeline'
|
|
13
13
|
require 'redis_client/cluster/error_identification'
|
|
14
|
+
require 'redis_client/cluster/router/routing_table'
|
|
14
15
|
|
|
15
16
|
class RedisClient
|
|
16
17
|
class Cluster
|
|
17
18
|
class Router
|
|
18
19
|
ZERO_CURSOR_FOR_SCAN = '0'
|
|
19
20
|
TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
|
|
20
|
-
|
|
21
|
-
PICK_FIRST = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
|
|
22
|
-
FLATTEN_STRINGS = ->(reply) { reply.flatten.sort_by(&:to_s) }
|
|
23
|
-
SUM_NUM = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
|
|
24
|
-
SORT_NUMBERS = ->(reply) { reply.sort_by(&:to_i) }
|
|
25
|
-
if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
|
|
26
|
-
Ractor.make_shareable(PICK_FIRST)
|
|
27
|
-
Ractor.make_shareable(FLATTEN_STRINGS)
|
|
28
|
-
Ractor.make_shareable(SUM_NUM)
|
|
29
|
-
Ractor.make_shareable(SORT_NUMBERS)
|
|
30
|
-
end
|
|
31
|
-
DEDICATED_ACTIONS = lambda do # rubocop:disable Metrics/BlockLength
|
|
32
|
-
multiple_key_action = RoutingAction.new(method_name: :send_multiple_keys_command)
|
|
33
|
-
all_node_first_action = RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST)
|
|
34
|
-
primary_first_action = RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST)
|
|
35
|
-
not_supported_action = RoutingAction.new(method_name: :fail_not_supported_command)
|
|
36
|
-
keyless_action = RoutingAction.new(method_name: :fail_keyless_command)
|
|
37
|
-
single_node_action = RoutingAction.new(method_name: :assign_node_and_send_command)
|
|
38
|
-
{
|
|
39
|
-
'ping' => RoutingAction.new(method_name: :send_ping_command, reply_transformer: PICK_FIRST),
|
|
40
|
-
'wait' => RoutingAction.new(method_name: :send_wait_command),
|
|
41
|
-
'keys' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: FLATTEN_STRINGS),
|
|
42
|
-
'dbsize' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: SUM_NUM),
|
|
43
|
-
'scan' => RoutingAction.new(method_name: :send_scan_command),
|
|
44
|
-
'lastsave' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SORT_NUMBERS),
|
|
45
|
-
'role' => RoutingAction.new(method_name: :send_command_to_all_nodes),
|
|
46
|
-
'config' => RoutingAction.new(method_name: :send_config_command),
|
|
47
|
-
'client' => RoutingAction.new(method_name: :send_client_command),
|
|
48
|
-
'cluster' => RoutingAction.new(method_name: :send_cluster_command),
|
|
49
|
-
'memory' => RoutingAction.new(method_name: :send_memory_command),
|
|
50
|
-
'script' => RoutingAction.new(method_name: :send_script_command),
|
|
51
|
-
'pubsub' => RoutingAction.new(method_name: :send_pubsub_command),
|
|
52
|
-
'watch' => RoutingAction.new(method_name: :send_watch_command),
|
|
53
|
-
'mget' => multiple_key_action,
|
|
54
|
-
'mset' => multiple_key_action,
|
|
55
|
-
'del' => multiple_key_action,
|
|
56
|
-
'acl' => all_node_first_action,
|
|
57
|
-
'auth' => all_node_first_action,
|
|
58
|
-
'bgrewriteaof' => all_node_first_action,
|
|
59
|
-
'bgsave' => all_node_first_action,
|
|
60
|
-
'quit' => all_node_first_action,
|
|
61
|
-
'save' => all_node_first_action,
|
|
62
|
-
'select' => all_node_first_action,
|
|
63
|
-
'flushall' => primary_first_action,
|
|
64
|
-
'flushdb' => primary_first_action,
|
|
65
|
-
# The redis 7.0 tags RANDOMKEY with `request_policy:all_shards` but without any response policy.
|
|
66
|
-
# It was corrected to `response_policy:special` in the redis 7.2.
|
|
67
|
-
# This entry keeps the historical single node routing for the redis 7.0.
|
|
68
|
-
'randomkey' => single_node_action,
|
|
69
|
-
'readonly' => not_supported_action,
|
|
70
|
-
'readwrite' => not_supported_action,
|
|
71
|
-
'shutdown' => not_supported_action,
|
|
72
|
-
'discard' => keyless_action,
|
|
73
|
-
'exec' => keyless_action,
|
|
74
|
-
'multi' => keyless_action,
|
|
75
|
-
'unwatch' => keyless_action
|
|
76
|
-
}.each_with_object({}) do |(k, v), acc|
|
|
77
|
-
acc[k] = v.freeze
|
|
78
|
-
acc[k.upcase] = v.freeze
|
|
79
|
-
end
|
|
80
|
-
end.call.freeze
|
|
81
|
-
|
|
82
|
-
# The routing which the command tips of the COMMAND command reply instruct.
|
|
83
|
-
# The `multi_shard` and the `special` request policies are out of scope.
|
|
84
|
-
# The `special` response policy is also out of scope because the aggregation is undefined,
|
|
85
|
-
# and the fan-out would change the return value of commands such as INFO.
|
|
86
|
-
# The `agg_min`, `agg_max` and `agg_logical_*` response policies are out of scope too:
|
|
87
|
-
# the only reachable command with them today is WAITAOF, whose reply is an array,
|
|
88
|
-
# and the aggregation of array replies is undefined. Such a command falls back to
|
|
89
|
-
# the single node routing until a command with a settled semantics appears.
|
|
90
|
-
# The entries of the DEDICATED_ACTIONS take precedence over these to keep the existing behavior.
|
|
91
|
-
# @see https://redis.io/docs/latest/develop/reference/command-tips/
|
|
92
|
-
POLICY_ACTIONS = {
|
|
93
|
-
'all_shards' => {
|
|
94
|
-
nil => RoutingAction.new(method_name: :send_command_to_primaries).freeze,
|
|
95
|
-
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST).freeze,
|
|
96
|
-
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
97
|
-
'agg_sum' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: SUM_NUM).freeze
|
|
98
|
-
}.freeze,
|
|
99
|
-
'all_nodes' => {
|
|
100
|
-
nil => RoutingAction.new(method_name: :send_command_to_all_nodes).freeze,
|
|
101
|
-
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST).freeze,
|
|
102
|
-
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
103
|
-
'agg_sum' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SUM_NUM).freeze
|
|
104
|
-
}.freeze
|
|
105
|
-
}.freeze
|
|
106
|
-
|
|
107
|
-
private_constant :ZERO_CURSOR_FOR_SCAN, :TSF, :RoutingAction, :PICK_FIRST, :FLATTEN_STRINGS,
|
|
108
|
-
:SUM_NUM, :SORT_NUMBERS, :DEDICATED_ACTIONS, :POLICY_ACTIONS
|
|
21
|
+
private_constant :ZERO_CURSOR_FOR_SCAN, :TSF
|
|
109
22
|
|
|
110
23
|
attr_reader :config
|
|
111
24
|
|
|
112
25
|
def initialize(config, concurrent_worker, pool: nil, **kwargs)
|
|
113
26
|
@config = config
|
|
27
|
+
@dedicated_actions = RoutingTable.build(config.command_routings)
|
|
114
28
|
@concurrent_worker = concurrent_worker
|
|
115
29
|
@pool = pool
|
|
116
30
|
@client_kwargs = kwargs
|
|
@@ -124,7 +38,7 @@ class RedisClient
|
|
|
124
38
|
end
|
|
125
39
|
|
|
126
40
|
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
127
|
-
action =
|
|
41
|
+
action = @dedicated_actions[command.first]
|
|
128
42
|
if action.nil?
|
|
129
43
|
cmd_spec = @command.get_spec(command)
|
|
130
44
|
action = find_policy_action(cmd_spec)
|
|
@@ -350,7 +264,7 @@ class RedisClient
|
|
|
350
264
|
request_policy = cmd_spec.request_policy
|
|
351
265
|
return if request_policy.nil?
|
|
352
266
|
|
|
353
|
-
|
|
267
|
+
RoutingTable.find_policy_action(request_policy, cmd_spec.response_policy)
|
|
354
268
|
end
|
|
355
269
|
|
|
356
270
|
def send_command_to_all_nodes(method, command, args, &block)
|
|
@@ -6,6 +6,7 @@ require 'redis_client/cluster'
|
|
|
6
6
|
require 'redis_client/cluster/errors'
|
|
7
7
|
require 'redis_client/cluster/node_key'
|
|
8
8
|
require 'redis_client/cluster/noop_command_builder'
|
|
9
|
+
require 'redis_client/cluster/router/routing_table'
|
|
9
10
|
require 'redis_client/command_builder'
|
|
10
11
|
|
|
11
12
|
class RedisClient
|
|
@@ -25,10 +26,18 @@ class RedisClient
|
|
|
25
26
|
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
|
26
27
|
# Controls the balance between startup load and stability during initialization or cluster state changes.
|
|
27
28
|
MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
|
|
29
|
+
VALID_COMMAND_ROUTING_KEYS = %i[request_policy response_policy].freeze
|
|
30
|
+
# Routing these could wedge the state of the connections in the pool,
|
|
31
|
+
# e.g. leave them inside a MULTI or a subscription.
|
|
32
|
+
UNSAFE_COMMAND_ROUTINGS = %w[
|
|
33
|
+
multi exec discard watch unwatch
|
|
34
|
+
subscribe unsubscribe psubscribe punsubscribe ssubscribe sunsubscribe
|
|
35
|
+
].freeze
|
|
28
36
|
|
|
29
37
|
private_constant :DEFAULT_HOST, :DEFAULT_PORT, :DEFAULT_SCHEME, :SECURE_SCHEME, :DEFAULT_NODES,
|
|
30
38
|
:VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
|
|
31
|
-
:MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
|
|
39
|
+
:MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE,
|
|
40
|
+
:VALID_COMMAND_ROUTING_KEYS, :UNSAFE_COMMAND_ROUTINGS
|
|
32
41
|
|
|
33
42
|
InvalidClientConfigError = Class.new(::RedisClient::Cluster::Error)
|
|
34
43
|
|
|
@@ -39,9 +48,9 @@ class RedisClient
|
|
|
39
48
|
private_constant :SENSITIVE_INSPECT_KEYS, :INSPECT_REDACTED_KEYS, :INSPECT_PLACEHOLDER
|
|
40
49
|
|
|
41
50
|
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
|
42
|
-
:connect_with_original_config, :startup_nodes, :max_startup_sample, :id
|
|
51
|
+
:connect_with_original_config, :startup_nodes, :max_startup_sample, :id, :command_routings
|
|
43
52
|
|
|
44
|
-
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
53
|
+
def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
45
54
|
nodes: DEFAULT_NODES,
|
|
46
55
|
replica: false,
|
|
47
56
|
replica_affinity: :random,
|
|
@@ -52,6 +61,7 @@ class RedisClient
|
|
|
52
61
|
slow_command_timeout: SLOW_COMMAND_TIMEOUT,
|
|
53
62
|
command_builder: ::RedisClient::CommandBuilder,
|
|
54
63
|
max_startup_sample: MAX_STARTUP_SAMPLE,
|
|
64
|
+
command_routings: nil,
|
|
55
65
|
**client_config
|
|
56
66
|
)
|
|
57
67
|
@replica = true & replica
|
|
@@ -67,6 +77,7 @@ class RedisClient
|
|
|
67
77
|
@client_implementation = client_implementation
|
|
68
78
|
@slow_command_timeout = slow_command_timeout
|
|
69
79
|
@max_startup_sample = max_startup_sample
|
|
80
|
+
@command_routings = normalize_command_routings(command_routings)
|
|
70
81
|
@id = client_config[:id]
|
|
71
82
|
end
|
|
72
83
|
|
|
@@ -197,6 +208,52 @@ class RedisClient
|
|
|
197
208
|
raise InvalidClientConfigError, e.message
|
|
198
209
|
end
|
|
199
210
|
|
|
211
|
+
def normalize_command_routings(routings)
|
|
212
|
+
return if routings.nil? || (routings.is_a?(Hash) && routings.empty?)
|
|
213
|
+
raise InvalidClientConfigError, "`command_routings` option must be a Hash: #{routings.class}" unless routings.is_a?(Hash)
|
|
214
|
+
|
|
215
|
+
routings.each_with_object({}) do |(name, value), acc|
|
|
216
|
+
acc[normalize_command_routing_name(name)] = normalize_command_routing_value(name, value)
|
|
217
|
+
end.freeze
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def normalize_command_routing_name(name)
|
|
221
|
+
raise InvalidClientConfigError, "`command_routings` option includes an invalid command name: #{name.inspect}" unless name.is_a?(String) || name.is_a?(Symbol)
|
|
222
|
+
|
|
223
|
+
key = name.to_s.downcase
|
|
224
|
+
raise InvalidClientConfigError, '`command_routings` option includes an empty command name' if key.empty?
|
|
225
|
+
raise InvalidClientConfigError, "`command_routings` option can route a command, not a subcommand: #{name.inspect}" if key.match?(/\s/)
|
|
226
|
+
raise InvalidClientConfigError, "`command_routings` option can't route the command which changes the state of a connection: #{key}" if UNSAFE_COMMAND_ROUTINGS.include?(key)
|
|
227
|
+
|
|
228
|
+
-key
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def normalize_command_routing_value(name, value)
|
|
232
|
+
return if value.nil? || (value.is_a?(Hash) && value.empty?)
|
|
233
|
+
raise InvalidClientConfigError, "`command_routings` option includes an invalid routing of #{name}: #{value.inspect}" unless value.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
normalize_command_routing_policies(name, value.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k })
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def normalize_command_routing_policies(name, policies)
|
|
239
|
+
unknown_keys = policies.keys - VALID_COMMAND_ROUTING_KEYS
|
|
240
|
+
raise InvalidClientConfigError, "`command_routings` option includes unknown keys of #{name}: #{unknown_keys.join(', ')}" unless unknown_keys.empty?
|
|
241
|
+
raise InvalidClientConfigError, "`command_routings` option needs the request_policy key of #{name}" if policies[:request_policy].nil?
|
|
242
|
+
|
|
243
|
+
build_command_routing_policies(name, policies[:request_policy], policies[:response_policy])
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def build_command_routing_policies(name, request_policy, response_policy)
|
|
247
|
+
request_policy = -request_policy.to_s
|
|
248
|
+
response_policy = response_policy.nil? ? nil : -response_policy.to_s
|
|
249
|
+
if ::RedisClient::Cluster::Router::RoutingTable.find_policy_action(request_policy, response_policy).nil?
|
|
250
|
+
raise InvalidClientConfigError,
|
|
251
|
+
"`command_routings` option includes unsupported policies of #{name}: request_policy=#{request_policy}, response_policy=#{response_policy.inspect}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
{ request_policy: request_policy, response_policy: response_policy }.freeze
|
|
255
|
+
end
|
|
256
|
+
|
|
200
257
|
def merge_generic_config(client_config, node_configs)
|
|
201
258
|
cfg = node_configs.first || {}
|
|
202
259
|
client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis-cluster-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Taishi Kasuga
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/redis_client/cluster/pipeline.rb
|
|
52
52
|
- lib/redis_client/cluster/pub_sub.rb
|
|
53
53
|
- lib/redis_client/cluster/router.rb
|
|
54
|
+
- lib/redis_client/cluster/router/routing_table.rb
|
|
54
55
|
- lib/redis_client/cluster/transaction.rb
|
|
55
56
|
- lib/redis_client/cluster_config.rb
|
|
56
57
|
- lib/redis_cluster_client.rb
|