redis-cluster-client 0.16.7 → 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
|
|
@@ -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
|