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.
@@ -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(@command_builder)
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, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
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('MULTI')
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('EXEC')
102
+ @pipeline.call('exec')
98
103
  settle
99
104
  end
100
105
 
101
106
  def cancel
102
- @pipeline.call('DISCARD')
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('ASKING') if @asking
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, "#{err.message}: #{err.command}"
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(ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}")
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('ASKING') == 'OK'
192
+ node.call('asking') == 'OK'
182
193
  rescue StandardError
183
194
  false
184
195
  end
@@ -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
- raise ArgumentError, 'block required' unless block
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('SCAN', cursor, *args, seed: seed, **kwargs)
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
- node = router.assign_node(['SSCAN', key])
76
- router.try_delegate(node, :sscan, key, *args, **kwargs, &block)
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
- node = router.assign_node(['HSCAN', key])
81
- router.try_delegate(node, :hscan, key, *args, **kwargs, &block)
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
- node = router.assign_node(['ZSCAN', key])
86
- router.try_delegate(node, :zscan, key, *args, **kwargs, &block)
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
- if router.command_exists?(name)
147
- args.unshift(name)
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
- DEFAULT_NODES = ["#{DEFAULT_SCHEME}://#{DEFAULT_HOST}:#{DEFAULT_PORT}"].freeze
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', 5))
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
- InvalidClientConfigError = Class.new(::RedisClient::Error)
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
- case option
95
- when Hash
96
- option = option.transform_keys(&:to_sym)
97
- { size: MAX_WORKERS }.merge(option)
98
- else { size: MAX_WORKERS }
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.11.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: 2024-08-19 00:00:00.000000000 Z
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.22'
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.22'
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/normalized_cmd_name.rb
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.5.11
81
- signing_key:
77
+ rubygems_version: 3.6.7
82
78
  specification_version: 4
83
- summary: A Redis cluster client for Ruby
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