redis-cluster-client 0.11.0 → 0.13.5
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 +47 -75
- data/lib/redis_client/cluster/concurrent_worker/on_demand.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker/pooled.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker.rb +4 -6
- data/lib/redis_client/cluster/errors.rb +39 -19
- data/lib/redis_client/cluster/key_slot_converter.rb +3 -1
- data/lib/redis_client/cluster/node/base_topology.rb +3 -1
- data/lib/redis_client/cluster/node/latency_replica.rb +3 -1
- data/lib/redis_client/cluster/node.rb +77 -14
- data/lib/redis_client/cluster/node_key.rb +2 -0
- data/lib/redis_client/cluster/noop_command_builder.rb +13 -0
- data/lib/redis_client/cluster/optimistic_locking.rb +25 -10
- data/lib/redis_client/cluster/pipeline.rb +53 -18
- data/lib/redis_client/cluster/pub_sub.rb +70 -31
- data/lib/redis_client/cluster/router.rb +282 -134
- data/lib/redis_client/cluster/transaction.rb +22 -11
- data/lib/redis_client/cluster.rb +24 -15
- data/lib/redis_client/cluster_config.rb +44 -11
- metadata +7 -11
- data/lib/redis_client/cluster/normalized_cmd_name.rb +0 -69
@@ -1,20 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
+
require 'redis_client/cluster/errors'
|
5
|
+
require 'redis_client/cluster/noop_command_builder'
|
4
6
|
require 'redis_client/cluster/pipeline'
|
5
7
|
|
6
8
|
class RedisClient
|
7
9
|
class Cluster
|
8
10
|
class Transaction
|
9
|
-
ConsistencyError = Class.new(::RedisClient::Error)
|
11
|
+
ConsistencyError = Class.new(::RedisClient::Cluster::Error)
|
12
|
+
|
10
13
|
MAX_REDIRECTION = 2
|
11
14
|
EMPTY_ARRAY = [].freeze
|
12
15
|
|
16
|
+
private_constant :MAX_REDIRECTION, :EMPTY_ARRAY
|
17
|
+
|
13
18
|
def initialize(router, command_builder, node: nil, slot: nil, asking: false)
|
14
19
|
@router = router
|
15
20
|
@command_builder = command_builder
|
16
21
|
@retryable = true
|
17
|
-
@pipeline = ::RedisClient::Pipeline.new(
|
22
|
+
@pipeline = ::RedisClient::Pipeline.new(::RedisClient::Cluster::NoopCommandBuilder)
|
18
23
|
@pending_commands = []
|
19
24
|
@node = node
|
20
25
|
prepare_tx unless @node.nil?
|
@@ -64,7 +69,7 @@ class RedisClient
|
|
64
69
|
@pending_commands.each(&:call)
|
65
70
|
|
66
71
|
return EMPTY_ARRAY if @pipeline._empty?
|
67
|
-
raise ConsistencyError
|
72
|
+
raise ConsistencyError.new("couldn't determine the node: #{@pipeline._commands}").with_config(@router.config) if @node.nil?
|
68
73
|
|
69
74
|
commit
|
70
75
|
end
|
@@ -88,24 +93,24 @@ class RedisClient
|
|
88
93
|
end
|
89
94
|
|
90
95
|
def prepare_tx
|
91
|
-
@pipeline.call('
|
96
|
+
@pipeline.call('multi')
|
92
97
|
@pending_commands.each(&:call)
|
93
98
|
@pending_commands.clear
|
94
99
|
end
|
95
100
|
|
96
101
|
def commit
|
97
|
-
@pipeline.call('
|
102
|
+
@pipeline.call('exec')
|
98
103
|
settle
|
99
104
|
end
|
100
105
|
|
101
106
|
def cancel
|
102
|
-
@pipeline.call('
|
107
|
+
@pipeline.call('discard')
|
103
108
|
settle
|
104
109
|
end
|
105
110
|
|
106
111
|
def settle
|
107
112
|
# If we needed ASKING on the watch, we need ASKING on the multi as well.
|
108
|
-
@node.call('
|
113
|
+
@node.call('asking') if @asking
|
109
114
|
# Don't handle redirections at this level if we're in a watch (the watcher handles redirections
|
110
115
|
# at the whole-transaction level.)
|
111
116
|
send_transaction(@node, redirect: !!@watching_slot ? 0 : MAX_REDIRECTION)
|
@@ -119,7 +124,7 @@ class RedisClient
|
|
119
124
|
end
|
120
125
|
end
|
121
126
|
|
122
|
-
def send_pipeline(client, redirect:)
|
127
|
+
def send_pipeline(client, redirect:) # rubocop:disable Metrics/AbcSize
|
123
128
|
replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
|
124
129
|
commands = @pipeline._commands
|
125
130
|
client.middlewares.call_pipelined(commands, client.config) do
|
@@ -135,6 +140,9 @@ class RedisClient
|
|
135
140
|
return if replies.last.nil?
|
136
141
|
|
137
142
|
coerce_results!(replies.last)
|
143
|
+
rescue ::RedisClient::ConnectionError
|
144
|
+
@router.renew_cluster_state if @watching_slot.nil?
|
145
|
+
raise
|
138
146
|
end
|
139
147
|
|
140
148
|
def coerce_results!(results, offset: 1)
|
@@ -157,13 +165,16 @@ class RedisClient
|
|
157
165
|
|
158
166
|
def handle_command_error!(err, redirect:) # rubocop:disable Metrics/AbcSize
|
159
167
|
if err.message.start_with?('CROSSSLOT')
|
160
|
-
raise ConsistencyError
|
168
|
+
raise ConsistencyError.new("#{err.message}: #{err.command}").with_config(@router.config)
|
161
169
|
elsif err.message.start_with?('MOVED')
|
162
170
|
node = @router.assign_redirection_node(err.message)
|
163
171
|
send_transaction(node, redirect: redirect - 1)
|
164
172
|
elsif err.message.start_with?('ASK')
|
165
173
|
node = @router.assign_asking_node(err.message)
|
166
174
|
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
|
175
|
+
elsif err.message.start_with?('CLUSTERDOWN')
|
176
|
+
@router.renew_cluster_state if @watching_slot.nil?
|
177
|
+
raise err
|
167
178
|
else
|
168
179
|
raise err
|
169
180
|
end
|
@@ -174,11 +185,11 @@ class RedisClient
|
|
174
185
|
return if slots.size == 1 && @watching_slot.nil?
|
175
186
|
return if slots.size == 1 && @watching_slot == slots.first
|
176
187
|
|
177
|
-
raise(
|
188
|
+
raise ConsistencyError.new("the transaction should be executed to a slot in a node: #{commands}").with_config(@router.config)
|
178
189
|
end
|
179
190
|
|
180
191
|
def try_asking(node)
|
181
|
-
node.call('
|
192
|
+
node.call('asking') == 'OK'
|
182
193
|
rescue StandardError
|
183
194
|
false
|
184
195
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -11,13 +11,14 @@ class RedisClient
|
|
11
11
|
class Cluster
|
12
12
|
ZERO_CURSOR_FOR_SCAN = '0'
|
13
13
|
|
14
|
+
private_constant :ZERO_CURSOR_FOR_SCAN
|
15
|
+
|
14
16
|
attr_reader :config
|
15
17
|
|
16
|
-
def initialize(config, pool: nil, concurrency: nil, **kwargs)
|
17
|
-
@config = config
|
18
|
+
def initialize(config = nil, pool: nil, concurrency: nil, **kwargs)
|
19
|
+
@config = config.nil? ? ClusterConfig.new(**kwargs) : config
|
18
20
|
@concurrent_worker = ::RedisClient::Cluster::ConcurrentWorker.create(**(concurrency || {}))
|
19
|
-
@command_builder = config.command_builder
|
20
|
-
|
21
|
+
@command_builder = @config.command_builder
|
21
22
|
@pool = pool
|
22
23
|
@kwargs = kwargs
|
23
24
|
@router = nil
|
@@ -60,30 +61,37 @@ class RedisClient
|
|
60
61
|
end
|
61
62
|
|
62
63
|
def scan(*args, **kwargs, &block)
|
63
|
-
|
64
|
+
return to_enum(__callee__, *args, **kwargs) unless block_given?
|
64
65
|
|
66
|
+
command = @command_builder.generate(['scan', ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
65
67
|
seed = Random.new_seed
|
66
|
-
cursor = ZERO_CURSOR_FOR_SCAN
|
67
68
|
loop do
|
68
|
-
cursor, keys = router.scan(
|
69
|
+
cursor, keys = router.scan(command, seed: seed)
|
70
|
+
command[1] = cursor
|
69
71
|
keys.each(&block)
|
70
72
|
break if cursor == ZERO_CURSOR_FOR_SCAN
|
71
73
|
end
|
72
74
|
end
|
73
75
|
|
74
76
|
def sscan(key, *args, **kwargs, &block)
|
75
|
-
|
76
|
-
|
77
|
+
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
78
|
+
|
79
|
+
command = @command_builder.generate(['sscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
80
|
+
router.scan_single_key(command, arity: 1, &block)
|
77
81
|
end
|
78
82
|
|
79
83
|
def hscan(key, *args, **kwargs, &block)
|
80
|
-
|
81
|
-
|
84
|
+
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
85
|
+
|
86
|
+
command = @command_builder.generate(['hscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
87
|
+
router.scan_single_key(command, arity: 2, &block)
|
82
88
|
end
|
83
89
|
|
84
90
|
def zscan(key, *args, **kwargs, &block)
|
85
|
-
|
86
|
-
|
91
|
+
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
92
|
+
|
93
|
+
command = @command_builder.generate(['zscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
94
|
+
router.scan_single_key(command, arity: 2, &block)
|
87
95
|
end
|
88
96
|
|
89
97
|
def pipelined(exception: true)
|
@@ -143,8 +151,9 @@ class RedisClient
|
|
143
151
|
end
|
144
152
|
|
145
153
|
def method_missing(name, *args, **kwargs, &block)
|
146
|
-
|
147
|
-
|
154
|
+
cmd = name.respond_to?(:name) ? name.name : name.to_s
|
155
|
+
if router.command_exists?(cmd)
|
156
|
+
args.unshift(cmd)
|
148
157
|
command = @command_builder.generate(args, kwargs)
|
149
158
|
return router.send_command(:call_v, command, &block)
|
150
159
|
end
|
@@ -3,7 +3,9 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'redis_client'
|
5
5
|
require 'redis_client/cluster'
|
6
|
+
require 'redis_client/cluster/errors'
|
6
7
|
require 'redis_client/cluster/node_key'
|
8
|
+
require 'redis_client/cluster/noop_command_builder'
|
7
9
|
require 'redis_client/command_builder'
|
8
10
|
|
9
11
|
class RedisClient
|
@@ -12,21 +14,27 @@ class RedisClient
|
|
12
14
|
DEFAULT_PORT = 6379
|
13
15
|
DEFAULT_SCHEME = 'redis'
|
14
16
|
SECURE_SCHEME = 'rediss'
|
15
|
-
|
17
|
+
DEFAULT_NODE = "#{DEFAULT_SCHEME}://#{DEFAULT_HOST}:#{DEFAULT_PORT}"
|
18
|
+
Ractor.make_shareable(DEFAULT_NODE) if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
|
19
|
+
DEFAULT_NODES = [DEFAULT_NODE].freeze
|
16
20
|
VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
|
17
21
|
VALID_NODES_KEYS = %i[ssl username password host port db].freeze
|
18
22
|
MERGE_CONFIG_KEYS = %i[ssl username password].freeze
|
19
23
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
20
|
-
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS',
|
24
|
+
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', -1)) # for backward compatibility
|
21
25
|
# It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
|
22
26
|
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
23
27
|
# It affects to strike a balance between load and stability in initialization or changed states.
|
24
28
|
MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
|
25
29
|
|
26
|
-
|
30
|
+
private_constant :DEFAULT_HOST, :DEFAULT_PORT, :DEFAULT_SCHEME, :SECURE_SCHEME, :DEFAULT_NODES,
|
31
|
+
:VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
|
32
|
+
:MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
|
33
|
+
|
34
|
+
InvalidClientConfigError = Class.new(::RedisClient::Cluster::Error)
|
27
35
|
|
28
36
|
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
29
|
-
:connect_with_original_config, :startup_nodes, :max_startup_sample
|
37
|
+
:connect_with_original_config, :startup_nodes, :max_startup_sample, :id
|
30
38
|
|
31
39
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
32
40
|
nodes: DEFAULT_NODES,
|
@@ -41,7 +49,6 @@ class RedisClient
|
|
41
49
|
max_startup_sample: MAX_STARTUP_SAMPLE,
|
42
50
|
**client_config
|
43
51
|
)
|
44
|
-
|
45
52
|
@replica = true & replica
|
46
53
|
@replica_affinity = replica_affinity.to_s.to_sym
|
47
54
|
@fixed_hostname = fixed_hostname.to_s
|
@@ -55,16 +62,25 @@ class RedisClient
|
|
55
62
|
@client_implementation = client_implementation
|
56
63
|
@slow_command_timeout = slow_command_timeout
|
57
64
|
@max_startup_sample = max_startup_sample
|
65
|
+
@id = client_config[:id]
|
58
66
|
end
|
59
67
|
|
60
68
|
def inspect
|
61
|
-
"#<#{self.class.name} #{startup_nodes.values}>"
|
69
|
+
"#<#{self.class.name} #{startup_nodes.values.map { |v| v.reject { |k| k == :command_builder } }}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
def connect_timeout
|
73
|
+
@client_config[:connect_timeout] || @client_config[:timeout] || ::RedisClient::Config::DEFAULT_TIMEOUT
|
62
74
|
end
|
63
75
|
|
64
76
|
def read_timeout
|
65
77
|
@client_config[:read_timeout] || @client_config[:timeout] || ::RedisClient::Config::DEFAULT_TIMEOUT
|
66
78
|
end
|
67
79
|
|
80
|
+
def write_timeout
|
81
|
+
@client_config[:write_timeout] || @client_config[:timeout] || ::RedisClient::Config::DEFAULT_TIMEOUT
|
82
|
+
end
|
83
|
+
|
68
84
|
def new_pool(size: 5, timeout: 5, **kwargs)
|
69
85
|
@client_implementation.new(
|
70
86
|
self,
|
@@ -88,15 +104,31 @@ class RedisClient
|
|
88
104
|
augment_client_config(config)
|
89
105
|
end
|
90
106
|
|
107
|
+
def resolved?
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
def sentinel?
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
def server_url
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
91
119
|
private
|
92
120
|
|
93
121
|
def merge_concurrency_option(option)
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
122
|
+
opts = {}
|
123
|
+
|
124
|
+
if MAX_WORKERS.positive?
|
125
|
+
opts[:model] = :on_demand
|
126
|
+
opts[:size] = MAX_WORKERS
|
99
127
|
end
|
128
|
+
|
129
|
+
opts.merge!(option.transform_keys(&:to_sym)) if option.is_a?(Hash)
|
130
|
+
opts[:model] = :none if opts.empty?
|
131
|
+
opts.freeze
|
100
132
|
end
|
101
133
|
|
102
134
|
def build_node_configs(addrs)
|
@@ -169,6 +201,7 @@ class RedisClient
|
|
169
201
|
def augment_client_config(config)
|
170
202
|
config = @client_config.merge(config)
|
171
203
|
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
204
|
+
config[:command_builder] = ::RedisClient::Cluster::NoopCommandBuilder # prevent twice call
|
172
205
|
config
|
173
206
|
end
|
174
207
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
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.13.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: redis-client
|
@@ -16,15 +15,14 @@ dependencies:
|
|
16
15
|
requirements:
|
17
16
|
- - "~>"
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
18
|
+
version: '0.24'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - "~>"
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
27
|
-
description:
|
25
|
+
version: '0.24'
|
28
26
|
email:
|
29
27
|
- proxy0721@gmail.com
|
30
28
|
executables: []
|
@@ -48,7 +46,7 @@ files:
|
|
48
46
|
- lib/redis_client/cluster/node/random_replica.rb
|
49
47
|
- lib/redis_client/cluster/node/random_replica_or_primary.rb
|
50
48
|
- lib/redis_client/cluster/node_key.rb
|
51
|
-
- lib/redis_client/cluster/
|
49
|
+
- lib/redis_client/cluster/noop_command_builder.rb
|
52
50
|
- lib/redis_client/cluster/optimistic_locking.rb
|
53
51
|
- lib/redis_client/cluster/pipeline.rb
|
54
52
|
- lib/redis_client/cluster/pub_sub.rb
|
@@ -62,7 +60,6 @@ licenses:
|
|
62
60
|
metadata:
|
63
61
|
rubygems_mfa_required: 'true'
|
64
62
|
allowed_push_host: https://rubygems.org
|
65
|
-
post_install_message:
|
66
63
|
rdoc_options: []
|
67
64
|
require_paths:
|
68
65
|
- lib
|
@@ -77,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
74
|
- !ruby/object:Gem::Version
|
78
75
|
version: '0'
|
79
76
|
requirements: []
|
80
|
-
rubygems_version: 3.
|
81
|
-
signing_key:
|
77
|
+
rubygems_version: 3.6.7
|
82
78
|
specification_version: 4
|
83
|
-
summary:
|
79
|
+
summary: Redis cluster-aware client for Ruby
|
84
80
|
test_files: []
|
@@ -1,69 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'singleton'
|
4
|
-
|
5
|
-
class RedisClient
|
6
|
-
class Cluster
|
7
|
-
class NormalizedCmdName
|
8
|
-
include Singleton
|
9
|
-
|
10
|
-
EMPTY_STRING = ''
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
@cache = {}
|
14
|
-
@mutex = Mutex.new
|
15
|
-
end
|
16
|
-
|
17
|
-
def get_by_command(command)
|
18
|
-
get(command, index: 0)
|
19
|
-
end
|
20
|
-
|
21
|
-
def get_by_subcommand(command)
|
22
|
-
get(command, index: 1)
|
23
|
-
end
|
24
|
-
|
25
|
-
def get_by_name(name)
|
26
|
-
get(name, index: 0)
|
27
|
-
end
|
28
|
-
|
29
|
-
def clear
|
30
|
-
@mutex.synchronize { @cache.clear }
|
31
|
-
true
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def get(command, index:)
|
37
|
-
name = extract_name(command, index: index)
|
38
|
-
return EMPTY_STRING if name.nil? || name.empty?
|
39
|
-
|
40
|
-
normalize(name)
|
41
|
-
end
|
42
|
-
|
43
|
-
def extract_name(command, index:)
|
44
|
-
case command
|
45
|
-
when String, Symbol then index.zero? ? command : nil
|
46
|
-
when Array then extract_name_from_array(command, index: index)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def extract_name_from_array(command, index:)
|
51
|
-
return if command.size - 1 < index
|
52
|
-
|
53
|
-
case e = command[index]
|
54
|
-
when String, Symbol then e
|
55
|
-
when Array then e[index]
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def normalize(name)
|
60
|
-
return @cache[name] || name.to_s.downcase if @cache.key?(name)
|
61
|
-
return name.to_s.downcase if @mutex.locked?
|
62
|
-
|
63
|
-
str = name.to_s.downcase
|
64
|
-
@mutex.synchronize { @cache[name] = str }
|
65
|
-
str
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|