redis-cluster-client 0.7.5 → 0.7.6
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/lib/redis_client/cluster/command.rb +53 -17
- data/lib/redis_client/cluster/error_identification.rb +49 -0
- data/lib/redis_client/cluster/key_slot_converter.rb +18 -0
- data/lib/redis_client/cluster/node/base_topology.rb +60 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +13 -17
- data/lib/redis_client/cluster/node/primary_only.rb +7 -19
- data/lib/redis_client/cluster/node/random_replica.rb +2 -4
- data/lib/redis_client/cluster/node/random_replica_or_primary.rb +2 -4
- data/lib/redis_client/cluster/node.rb +169 -109
- data/lib/redis_client/cluster/pinning_node.rb +35 -0
- data/lib/redis_client/cluster/pipeline.rb +19 -18
- data/lib/redis_client/cluster/router.rb +49 -83
- data/lib/redis_client/cluster/transaction.rb +158 -20
- data/lib/redis_client/cluster.rb +28 -2
- data/lib/redis_client/cluster_config.rb +30 -45
- metadata +6 -4
- data/lib/redis_client/cluster/node/replica_mixin.rb +0 -37
@@ -1,56 +1,194 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
+
require 'redis_client/cluster/pipeline'
|
5
|
+
require 'redis_client/cluster/key_slot_converter'
|
4
6
|
|
5
7
|
class RedisClient
|
6
8
|
class Cluster
|
7
9
|
class Transaction
|
8
10
|
ConsistencyError = Class.new(::RedisClient::Error)
|
9
11
|
|
10
|
-
def initialize(router, command_builder)
|
12
|
+
def initialize(router, command_builder, watch)
|
11
13
|
@router = router
|
12
14
|
@command_builder = command_builder
|
13
|
-
@
|
15
|
+
@watch = watch
|
16
|
+
@retryable = true
|
17
|
+
@pipeline = ::RedisClient::Pipeline.new(@command_builder)
|
18
|
+
@buffer = []
|
19
|
+
@node = nil
|
14
20
|
end
|
15
21
|
|
16
|
-
def call(*command, **kwargs, &
|
22
|
+
def call(*command, **kwargs, &block)
|
17
23
|
command = @command_builder.generate(command, kwargs)
|
18
|
-
|
24
|
+
if prepare(command)
|
25
|
+
@pipeline.call_v(command, &block)
|
26
|
+
else
|
27
|
+
@buffer << -> { @pipeline.call_v(command, &block) }
|
28
|
+
end
|
19
29
|
end
|
20
30
|
|
21
|
-
def call_v(command, &
|
31
|
+
def call_v(command, &block)
|
22
32
|
command = @command_builder.generate(command)
|
23
|
-
|
33
|
+
if prepare(command)
|
34
|
+
@pipeline.call_v(command, &block)
|
35
|
+
else
|
36
|
+
@buffer << -> { @pipeline.call_v(command, &block) }
|
37
|
+
end
|
24
38
|
end
|
25
39
|
|
26
|
-
def call_once(*command, **kwargs, &
|
40
|
+
def call_once(*command, **kwargs, &block)
|
41
|
+
@retryable = false
|
27
42
|
command = @command_builder.generate(command, kwargs)
|
28
|
-
|
43
|
+
if prepare(command)
|
44
|
+
@pipeline.call_once_v(command, &block)
|
45
|
+
else
|
46
|
+
@buffer << -> { @pipeline.call_once_v(command, &block) }
|
47
|
+
end
|
29
48
|
end
|
30
49
|
|
31
|
-
def call_once_v(command, &
|
50
|
+
def call_once_v(command, &block)
|
51
|
+
@retryable = false
|
32
52
|
command = @command_builder.generate(command)
|
33
|
-
|
53
|
+
if prepare(command)
|
54
|
+
@pipeline.call_once_v(command, &block)
|
55
|
+
else
|
56
|
+
@buffer << -> { @pipeline.call_once_v(command, &block) }
|
57
|
+
end
|
34
58
|
end
|
35
59
|
|
36
|
-
def execute
|
37
|
-
|
38
|
-
raise ArgumentError, 'empty transaction' if @node_key.nil?
|
60
|
+
def execute
|
61
|
+
@buffer.each(&:call)
|
39
62
|
|
40
|
-
|
41
|
-
|
63
|
+
raise ArgumentError, 'empty transaction' if @pipeline._empty?
|
64
|
+
raise ConsistencyError, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
|
65
|
+
raise ConsistencyError, "unsafe watch: #{@watch.join(' ')}" unless safe_watch?
|
66
|
+
|
67
|
+
settle
|
42
68
|
end
|
43
69
|
|
44
70
|
private
|
45
71
|
|
46
|
-
def
|
72
|
+
def watch?
|
73
|
+
!@watch.nil? && !@watch.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
def safe_watch?
|
77
|
+
return true unless watch?
|
78
|
+
return false if @node.nil?
|
79
|
+
|
80
|
+
slots = @watch.map do |k|
|
81
|
+
return false if k.nil? || k.empty?
|
82
|
+
|
83
|
+
::RedisClient::Cluster::KeySlotConverter.convert(k)
|
84
|
+
end
|
85
|
+
|
86
|
+
return false if slots.uniq.size != 1
|
87
|
+
|
88
|
+
@router.find_primary_node_by_slot(slots.first) == @node
|
89
|
+
end
|
90
|
+
|
91
|
+
def prepare(command)
|
92
|
+
return true unless @node.nil?
|
93
|
+
|
47
94
|
node_key = @router.find_primary_node_key(command)
|
48
|
-
|
95
|
+
return false if node_key.nil?
|
96
|
+
|
97
|
+
@node = @router.find_node(node_key)
|
98
|
+
@pipeline.call('WATCH', *@watch) if watch?
|
99
|
+
@pipeline.call('MULTI')
|
100
|
+
@buffer.each(&:call)
|
101
|
+
@buffer.clear
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def settle
|
106
|
+
@pipeline.call('EXEC')
|
107
|
+
@pipeline.call('UNWATCH') if watch?
|
108
|
+
send_transaction(@node, redirect: true)
|
109
|
+
end
|
110
|
+
|
111
|
+
def send_transaction(client, redirect:)
|
112
|
+
case client
|
113
|
+
when ::RedisClient then send_pipeline(client, redirect: redirect)
|
114
|
+
when ::RedisClient::Pooled then client.with { |c| send_pipeline(c, redirect: redirect) }
|
115
|
+
else raise NotImplementedError, "#{client.class.name}#multi for cluster client"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def send_pipeline(client, redirect:)
|
120
|
+
replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
|
121
|
+
commands = @pipeline._commands
|
122
|
+
client.middlewares.call_pipelined(commands, client.config) do
|
123
|
+
connection.call_pipelined(commands, nil)
|
124
|
+
rescue ::RedisClient::CommandError => e
|
125
|
+
return handle_command_error!(commands, e) if redirect
|
49
126
|
|
50
|
-
|
51
|
-
|
127
|
+
raise
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
offset = watch? ? 2 : 1
|
132
|
+
coerce_results!(replies[-offset], offset)
|
133
|
+
end
|
134
|
+
|
135
|
+
def coerce_results!(results, offset)
|
136
|
+
results.each_with_index do |result, index|
|
137
|
+
if result.is_a?(::RedisClient::CommandError)
|
138
|
+
result._set_command(@pipeline._commands[index + offset])
|
139
|
+
raise result
|
140
|
+
end
|
141
|
+
|
142
|
+
next if @pipeline._blocks.nil?
|
143
|
+
|
144
|
+
block = @pipeline._blocks[index + offset]
|
145
|
+
next if block.nil?
|
146
|
+
|
147
|
+
results[index] = block.call(result)
|
148
|
+
end
|
149
|
+
|
150
|
+
results
|
151
|
+
end
|
152
|
+
|
153
|
+
def handle_command_error!(commands, err)
|
154
|
+
if err.message.start_with?('CROSSSLOT')
|
155
|
+
raise ConsistencyError, "#{err.message}: #{err.command}"
|
156
|
+
elsif err.message.start_with?('MOVED', 'ASK')
|
157
|
+
ensure_the_same_node!(commands)
|
158
|
+
handle_redirection(err)
|
159
|
+
else
|
160
|
+
raise err
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def ensure_the_same_node!(commands)
|
165
|
+
commands.each do |command|
|
166
|
+
node_key = @router.find_primary_node_key(command)
|
167
|
+
next if node_key.nil?
|
168
|
+
|
169
|
+
node = @router.find_node(node_key)
|
170
|
+
next if @node == node
|
171
|
+
|
172
|
+
raise ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def handle_redirection(err)
|
177
|
+
if err.message.start_with?('MOVED')
|
178
|
+
node = @router.assign_redirection_node(err.message)
|
179
|
+
send_transaction(node, redirect: false)
|
180
|
+
elsif err.message.start_with?('ASK')
|
181
|
+
node = @router.assign_asking_node(err.message)
|
182
|
+
try_asking(node) ? send_transaction(node, redirect: false) : err
|
183
|
+
else
|
184
|
+
raise err
|
185
|
+
end
|
186
|
+
end
|
52
187
|
|
53
|
-
|
188
|
+
def try_asking(node)
|
189
|
+
node.call('ASKING') == 'OK'
|
190
|
+
rescue StandardError
|
191
|
+
false
|
54
192
|
end
|
55
193
|
end
|
56
194
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -5,6 +5,7 @@ require 'redis_client/cluster/pipeline'
|
|
5
5
|
require 'redis_client/cluster/pub_sub'
|
6
6
|
require 'redis_client/cluster/router'
|
7
7
|
require 'redis_client/cluster/transaction'
|
8
|
+
require 'redis_client/cluster/pinning_node'
|
8
9
|
|
9
10
|
class RedisClient
|
10
11
|
class Cluster
|
@@ -89,14 +90,26 @@ class RedisClient
|
|
89
90
|
pipeline.execute
|
90
91
|
end
|
91
92
|
|
92
|
-
def multi(watch: nil
|
93
|
-
::RedisClient::Cluster::Transaction.new(@router, @command_builder
|
93
|
+
def multi(watch: nil)
|
94
|
+
transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder, watch)
|
95
|
+
yield transaction
|
96
|
+
transaction.execute
|
94
97
|
end
|
95
98
|
|
96
99
|
def pubsub
|
97
100
|
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
|
98
101
|
end
|
99
102
|
|
103
|
+
# TODO: This isn't an official public interface yet. Don't use in your production environment.
|
104
|
+
# @see https://github.com/redis-rb/redis-cluster-client/issues/299
|
105
|
+
def with(key: nil, hashtag: nil, write: true, _retry_count: 0, &_)
|
106
|
+
key = process_with_arguments(key, hashtag)
|
107
|
+
|
108
|
+
node_key = @router.find_node_key_by_key(key, primary: write)
|
109
|
+
node = @router.find_node(node_key)
|
110
|
+
yield ::RedisClient::Cluster::PinningNode.new(node)
|
111
|
+
end
|
112
|
+
|
100
113
|
def close
|
101
114
|
@concurrent_worker.close
|
102
115
|
@router.close
|
@@ -105,6 +118,19 @@ class RedisClient
|
|
105
118
|
|
106
119
|
private
|
107
120
|
|
121
|
+
def process_with_arguments(key, hashtag) # rubocop:disable Metrics/CyclomaticComplexity
|
122
|
+
raise ArgumentError, 'Only one of key or hashtag may be provided' if key && hashtag
|
123
|
+
|
124
|
+
if hashtag
|
125
|
+
# The documentation says not to wrap your hashtag in {}, but people will probably
|
126
|
+
# do it anyway and it's easy for us to fix here.
|
127
|
+
key = hashtag&.match?(/^{.*}$/) ? hashtag : "{#{hashtag}}"
|
128
|
+
end
|
129
|
+
raise ArgumentError, 'One of key or hashtag must be provided' if key.nil? || key.empty?
|
130
|
+
|
131
|
+
key
|
132
|
+
end
|
133
|
+
|
108
134
|
def method_missing(name, *args, **kwargs, &block)
|
109
135
|
if @router.command_exists?(name)
|
110
136
|
args.unshift(name)
|
@@ -23,9 +23,10 @@ class RedisClient
|
|
23
23
|
|
24
24
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
25
25
|
|
26
|
-
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
26
|
+
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
27
|
+
:connect_with_original_config, :startup_nodes
|
27
28
|
|
28
|
-
def initialize(
|
29
|
+
def initialize(
|
29
30
|
nodes: DEFAULT_NODES,
|
30
31
|
replica: false,
|
31
32
|
replica_affinity: :random,
|
@@ -34,39 +35,26 @@ class RedisClient
|
|
34
35
|
connect_with_original_config: false,
|
35
36
|
client_implementation: ::RedisClient::Cluster, # for redis gem
|
36
37
|
slow_command_timeout: SLOW_COMMAND_TIMEOUT,
|
38
|
+
command_builder: ::RedisClient::CommandBuilder,
|
37
39
|
**client_config
|
38
40
|
)
|
39
41
|
|
40
42
|
@replica = true & replica
|
41
43
|
@replica_affinity = replica_affinity.to_s.to_sym
|
42
44
|
@fixed_hostname = fixed_hostname.to_s
|
43
|
-
@
|
44
|
-
|
45
|
-
@
|
46
|
-
|
45
|
+
@command_builder = command_builder
|
46
|
+
node_configs = build_node_configs(nodes.dup)
|
47
|
+
@client_config = merge_generic_config(client_config, node_configs)
|
48
|
+
# Keep tabs on the original startup nodes we were constructed with
|
49
|
+
@startup_nodes = build_startup_nodes(node_configs)
|
47
50
|
@concurrency = merge_concurrency_option(concurrency)
|
48
51
|
@connect_with_original_config = connect_with_original_config
|
49
52
|
@client_implementation = client_implementation
|
50
53
|
@slow_command_timeout = slow_command_timeout
|
51
|
-
@mutex = Mutex.new
|
52
|
-
end
|
53
|
-
|
54
|
-
def dup
|
55
|
-
self.class.new(
|
56
|
-
nodes: @node_configs,
|
57
|
-
replica: @replica,
|
58
|
-
replica_affinity: @replica_affinity,
|
59
|
-
fixed_hostname: @fixed_hostname,
|
60
|
-
concurrency: @concurrency,
|
61
|
-
connect_with_original_config: @connect_with_original_config,
|
62
|
-
client_implementation: @client_implementation,
|
63
|
-
slow_command_timeout: @slow_command_timeout,
|
64
|
-
**@client_config
|
65
|
-
)
|
66
54
|
end
|
67
55
|
|
68
56
|
def inspect
|
69
|
-
"#<#{self.class.name} #{
|
57
|
+
"#<#{self.class.name} #{startup_nodes.values}>"
|
70
58
|
end
|
71
59
|
|
72
60
|
def read_timeout
|
@@ -86,29 +74,14 @@ class RedisClient
|
|
86
74
|
@client_implementation.new(self, concurrency: @concurrency, **kwargs)
|
87
75
|
end
|
88
76
|
|
89
|
-
def per_node_key
|
90
|
-
@node_configs.to_h do |config|
|
91
|
-
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
92
|
-
config = @client_config.merge(config)
|
93
|
-
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
94
|
-
[node_key, config]
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
77
|
def use_replica?
|
99
78
|
@replica
|
100
79
|
end
|
101
80
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
def add_node(host, port)
|
109
|
-
return if @mutex.locked?
|
110
|
-
|
111
|
-
@mutex.synchronize { @node_configs << { host: host, port: port } }
|
81
|
+
def client_config_for_node(node_key)
|
82
|
+
config = ::RedisClient::Cluster::NodeKey.hashify(node_key)
|
83
|
+
config[:port] = ensure_integer(config[:port])
|
84
|
+
augment_client_config(config)
|
112
85
|
end
|
113
86
|
|
114
87
|
private
|
@@ -176,11 +149,23 @@ class RedisClient
|
|
176
149
|
end
|
177
150
|
|
178
151
|
def merge_generic_config(client_config, node_configs)
|
179
|
-
|
152
|
+
cfg = node_configs.first || {}
|
153
|
+
client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
154
|
+
.merge(cfg.slice(*MERGE_CONFIG_KEYS))
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_startup_nodes(configs)
|
158
|
+
configs.to_h do |config|
|
159
|
+
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
160
|
+
config = augment_client_config(config)
|
161
|
+
[node_key, config]
|
162
|
+
end
|
163
|
+
end
|
180
164
|
|
181
|
-
|
182
|
-
|
183
|
-
|
165
|
+
def augment_client_config(config)
|
166
|
+
config = @client_config.merge(config)
|
167
|
+
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
168
|
+
config
|
184
169
|
end
|
185
170
|
end
|
186
171
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-cluster-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -38,16 +38,18 @@ files:
|
|
38
38
|
- lib/redis_client/cluster/concurrent_worker/none.rb
|
39
39
|
- lib/redis_client/cluster/concurrent_worker/on_demand.rb
|
40
40
|
- lib/redis_client/cluster/concurrent_worker/pooled.rb
|
41
|
+
- lib/redis_client/cluster/error_identification.rb
|
41
42
|
- lib/redis_client/cluster/errors.rb
|
42
43
|
- lib/redis_client/cluster/key_slot_converter.rb
|
43
44
|
- lib/redis_client/cluster/node.rb
|
45
|
+
- lib/redis_client/cluster/node/base_topology.rb
|
44
46
|
- lib/redis_client/cluster/node/latency_replica.rb
|
45
47
|
- lib/redis_client/cluster/node/primary_only.rb
|
46
48
|
- lib/redis_client/cluster/node/random_replica.rb
|
47
49
|
- lib/redis_client/cluster/node/random_replica_or_primary.rb
|
48
|
-
- lib/redis_client/cluster/node/replica_mixin.rb
|
49
50
|
- lib/redis_client/cluster/node_key.rb
|
50
51
|
- lib/redis_client/cluster/normalized_cmd_name.rb
|
52
|
+
- lib/redis_client/cluster/pinning_node.rb
|
51
53
|
- lib/redis_client/cluster/pipeline.rb
|
52
54
|
- lib/redis_client/cluster/pub_sub.rb
|
53
55
|
- lib/redis_client/cluster/router.rb
|
@@ -75,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
77
|
- !ruby/object:Gem::Version
|
76
78
|
version: '0'
|
77
79
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
80
|
+
rubygems_version: 3.5.3
|
79
81
|
signing_key:
|
80
82
|
specification_version: 4
|
81
83
|
summary: A Redis cluster client for Ruby
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class RedisClient
|
4
|
-
class Cluster
|
5
|
-
class Node
|
6
|
-
module ReplicaMixin
|
7
|
-
attr_reader :clients, :primary_clients
|
8
|
-
|
9
|
-
EMPTY_ARRAY = [].freeze
|
10
|
-
|
11
|
-
def initialize(replications, options, pool, _concurrent_worker, **kwargs)
|
12
|
-
@replications = replications
|
13
|
-
@primary_node_keys = @replications.keys.sort
|
14
|
-
@replica_node_keys = @replications.values.flatten.sort
|
15
|
-
@clients = build_clients(@primary_node_keys, options, pool, **kwargs)
|
16
|
-
@primary_clients = @clients.select { |k, _| @primary_node_keys.include?(k) }
|
17
|
-
end
|
18
|
-
|
19
|
-
def any_primary_node_key(seed: nil)
|
20
|
-
random = seed.nil? ? Random : Random.new(seed)
|
21
|
-
@primary_node_keys.sample(random: random)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def build_clients(primary_node_keys, options, pool, **kwargs)
|
27
|
-
options.to_h do |node_key, option|
|
28
|
-
option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
|
29
|
-
config = ::RedisClient::Cluster::Node::Config.new(scale_read: !primary_node_keys.include?(node_key), **option)
|
30
|
-
client = pool.nil? ? config.new_client : config.new_pool(**pool)
|
31
|
-
[node_key, client]
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|