redis-cluster-client 0.7.6 → 0.7.8

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: 071a90a4437e12104fba1359e1a88fd64a0aa253f53ec1fdfd5c8b1fa62c63a5
4
- data.tar.gz: 773243dc28547cc114730cbef7f0f971dd7bb178d4ea9a8cb2e8c4924f99bf40
3
+ metadata.gz: 41d87ce45a108ff8ec7d3c17805276d5ba7df11d577de3f7d6b1176bdd297ed0
4
+ data.tar.gz: 7118539907bb755225ba908cf160ad8af73aeb5204820682ca3fc3a67813f3cb
5
5
  SHA512:
6
- metadata.gz: d345690443bc35cf19935ae9e886e0833bee49b772f21abdfb4752629b7c436100e6a367b730055fbd155d59924cfd4d76ad3df2ee1a0ed3aff65f097a82a208
7
- data.tar.gz: d76e747b3eec56184f253f40f82599f492338fe86d4d3c3d30e9cda22df087e6817673b7bf9f02c07192b77322efd7d204ff60ced77be7fe2e8764cf9d104f63
6
+ metadata.gz: 96e3c0f9c2860002642e079b756065e32f04891cd793dce9efcb74bb87a13683b14fee13c039693d99599549fee7ace68d54561b8c730d5e9eb7bb937a82c57d
7
+ data.tar.gz: 2e512b774b5778e3f7a2a3557bb45d97971d209c787494a19d81e0d8146f5d51bbef7940f37fa38f7587e1037185e23fa27ca8d48a46225ced6640be8425198e
@@ -31,6 +31,10 @@ class RedisClient
31
31
  def build_from_host_port(host, port)
32
32
  "#{host}#{DELIMITER}#{port}"
33
33
  end
34
+
35
+ def build_from_client(client)
36
+ "#{client.config.host}#{DELIMITER}#{client.config.port}"
37
+ end
34
38
  end
35
39
  end
36
40
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client'
4
+ require 'redis_client/cluster/key_slot_converter'
5
+ require 'redis_client/cluster/transaction'
6
+
7
+ class RedisClient
8
+ class Cluster
9
+ class OptimisticLocking
10
+ def initialize(router)
11
+ @router = router
12
+ end
13
+
14
+ def watch(keys)
15
+ ensure_safe_keys(keys)
16
+ node = find_node(keys)
17
+ cnt = 0 # We assume redirects occurred when incrementing it.
18
+
19
+ @router.handle_redirection(node, retry_count: 1) do |nd|
20
+ cnt += 1
21
+ nd.with do |c|
22
+ c.call('WATCH', *keys)
23
+ reply = yield(c, cnt > 1)
24
+ c.call('UNWATCH')
25
+ reply
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def ensure_safe_keys(keys)
33
+ return if safe?(keys)
34
+
35
+ raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}"
36
+ end
37
+
38
+ def safe?(keys)
39
+ return false if keys.empty?
40
+
41
+ slots = keys.map do |k|
42
+ return false if k.nil? || k.empty?
43
+
44
+ ::RedisClient::Cluster::KeySlotConverter.convert(k)
45
+ end
46
+
47
+ slots.uniq.size == 1
48
+ end
49
+
50
+ def find_node(keys)
51
+ node_key = @router.find_primary_node_key(['WATCH', *keys])
52
+ return @router.find_node(node_key) unless node_key.nil?
53
+
54
+ raise ::RedisClient::Cluster::Transaction::ConsistencyError, "couldn't determine the node"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -2,21 +2,23 @@
2
2
 
3
3
  require 'redis_client'
4
4
  require 'redis_client/cluster/pipeline'
5
- require 'redis_client/cluster/key_slot_converter'
5
+ require 'redis_client/cluster/node_key'
6
6
 
7
7
  class RedisClient
8
8
  class Cluster
9
9
  class Transaction
10
10
  ConsistencyError = Class.new(::RedisClient::Error)
11
+ MAX_REDIRECTION = 2
11
12
 
12
- def initialize(router, command_builder, watch)
13
+ def initialize(router, command_builder, node: nil, resharding: false)
13
14
  @router = router
14
15
  @command_builder = command_builder
15
- @watch = watch
16
16
  @retryable = true
17
17
  @pipeline = ::RedisClient::Pipeline.new(@command_builder)
18
- @buffer = []
19
- @node = nil
18
+ @pending_commands = []
19
+ @node = node
20
+ prepare_tx unless @node.nil?
21
+ @resharding_state = resharding
20
22
  end
21
23
 
22
24
  def call(*command, **kwargs, &block)
@@ -24,7 +26,7 @@ class RedisClient
24
26
  if prepare(command)
25
27
  @pipeline.call_v(command, &block)
26
28
  else
27
- @buffer << -> { @pipeline.call_v(command, &block) }
29
+ defer { @pipeline.call_v(command, &block) }
28
30
  end
29
31
  end
30
32
 
@@ -33,7 +35,7 @@ class RedisClient
33
35
  if prepare(command)
34
36
  @pipeline.call_v(command, &block)
35
37
  else
36
- @buffer << -> { @pipeline.call_v(command, &block) }
38
+ defer { @pipeline.call_v(command, &block) }
37
39
  end
38
40
  end
39
41
 
@@ -43,7 +45,7 @@ class RedisClient
43
45
  if prepare(command)
44
46
  @pipeline.call_once_v(command, &block)
45
47
  else
46
- @buffer << -> { @pipeline.call_once_v(command, &block) }
48
+ defer { @pipeline.call_once_v(command, &block) }
47
49
  end
48
50
  end
49
51
 
@@ -53,39 +55,24 @@ class RedisClient
53
55
  if prepare(command)
54
56
  @pipeline.call_once_v(command, &block)
55
57
  else
56
- @buffer << -> { @pipeline.call_once_v(command, &block) }
58
+ defer { @pipeline.call_once_v(command, &block) }
57
59
  end
58
60
  end
59
61
 
60
62
  def execute
61
- @buffer.each(&:call)
63
+ @pending_commands.each(&:call)
62
64
 
63
65
  raise ArgumentError, 'empty transaction' if @pipeline._empty?
64
66
  raise ConsistencyError, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
65
- raise ConsistencyError, "unsafe watch: #{@watch.join(' ')}" unless safe_watch?
66
67
 
67
68
  settle
68
69
  end
69
70
 
70
71
  private
71
72
 
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
73
+ def defer(&block)
74
+ @pending_commands << block
75
+ nil
89
76
  end
90
77
 
91
78
  def prepare(command)
@@ -95,17 +82,19 @@ class RedisClient
95
82
  return false if node_key.nil?
96
83
 
97
84
  @node = @router.find_node(node_key)
98
- @pipeline.call('WATCH', *@watch) if watch?
99
- @pipeline.call('MULTI')
100
- @buffer.each(&:call)
101
- @buffer.clear
85
+ prepare_tx
102
86
  true
103
87
  end
104
88
 
89
+ def prepare_tx
90
+ @pipeline.call('MULTI')
91
+ @pending_commands.each(&:call)
92
+ @pending_commands.clear
93
+ end
94
+
105
95
  def settle
106
96
  @pipeline.call('EXEC')
107
- @pipeline.call('UNWATCH') if watch?
108
- send_transaction(@node, redirect: true)
97
+ send_transaction(@node, redirect: MAX_REDIRECTION)
109
98
  end
110
99
 
111
100
  def send_transaction(client, redirect:)
@@ -122,17 +111,18 @@ class RedisClient
122
111
  client.middlewares.call_pipelined(commands, client.config) do
123
112
  connection.call_pipelined(commands, nil)
124
113
  rescue ::RedisClient::CommandError => e
125
- return handle_command_error!(commands, e) if redirect
114
+ return handle_command_error!(client, commands, e, redirect: redirect) unless redirect.zero?
126
115
 
127
116
  raise
128
117
  end
129
118
  end
130
119
 
131
- offset = watch? ? 2 : 1
132
- coerce_results!(replies[-offset], offset)
120
+ return if replies.last.nil?
121
+
122
+ coerce_results!(replies.last)
133
123
  end
134
124
 
135
- def coerce_results!(results, offset)
125
+ def coerce_results!(results, offset: 1)
136
126
  results.each_with_index do |result, index|
137
127
  if result.is_a?(::RedisClient::CommandError)
138
128
  result._set_command(@pipeline._commands[index + offset])
@@ -150,39 +140,30 @@ class RedisClient
150
140
  results
151
141
  end
152
142
 
153
- def handle_command_error!(commands, err)
143
+ def handle_command_error!(client, commands, err, redirect:) # rubocop:disable Metrics/AbcSize
154
144
  if err.message.start_with?('CROSSSLOT')
155
145
  raise ConsistencyError, "#{err.message}: #{err.command}"
156
- elsif err.message.start_with?('MOVED', 'ASK')
157
- ensure_the_same_node!(commands)
158
- handle_redirection(err)
146
+ elsif err.message.start_with?('MOVED')
147
+ ensure_the_same_node!(client, commands)
148
+ node = @router.assign_redirection_node(err.message)
149
+ send_transaction(node, redirect: redirect - 1)
150
+ elsif err.message.start_with?('ASK')
151
+ ensure_the_same_node!(client, commands)
152
+ node = @router.assign_asking_node(err.message)
153
+ try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
159
154
  else
160
155
  raise err
161
156
  end
162
157
  end
163
158
 
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?
159
+ def ensure_the_same_node!(client, commands)
160
+ node_keys = commands.map { |command| @router.find_primary_node_key(command) }.compact.uniq
161
+ expected_node_key = ::RedisClient::Cluster::NodeKey.build_from_client(client)
168
162
 
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
163
+ return if !@resharding_state && node_keys.size == 1 && node_keys.first == expected_node_key
164
+ return if @resharding_state && node_keys.size == 1
175
165
 
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
166
+ raise(ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}")
186
167
  end
187
168
 
188
169
  def try_asking(node)
@@ -6,6 +6,7 @@ require 'redis_client/cluster/pub_sub'
6
6
  require 'redis_client/cluster/router'
7
7
  require 'redis_client/cluster/transaction'
8
8
  require 'redis_client/cluster/pinning_node'
9
+ require 'redis_client/cluster/optimistic_locking'
9
10
 
10
11
  class RedisClient
11
12
  class Cluster
@@ -91,9 +92,19 @@ class RedisClient
91
92
  end
92
93
 
93
94
  def multi(watch: nil)
94
- transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder, watch)
95
- yield transaction
96
- transaction.execute
95
+ if watch.nil? || watch.empty?
96
+ transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder)
97
+ yield transaction
98
+ return transaction.execute
99
+ end
100
+
101
+ ::RedisClient::Cluster::OptimisticLocking.new(@router).watch(watch) do |c, resharding|
102
+ transaction = ::RedisClient::Cluster::Transaction.new(
103
+ @router, @command_builder, node: c, resharding: resharding
104
+ )
105
+ yield transaction
106
+ transaction.execute
107
+ end
97
108
  end
98
109
 
99
110
  def pubsub
@@ -102,12 +113,11 @@ class RedisClient
102
113
 
103
114
  # TODO: This isn't an official public interface yet. Don't use in your production environment.
104
115
  # @see https://github.com/redis-rb/redis-cluster-client/issues/299
105
- def with(key: nil, hashtag: nil, write: true, _retry_count: 0, &_)
116
+ def with(key: nil, hashtag: nil, write: true)
106
117
  key = process_with_arguments(key, hashtag)
107
-
108
118
  node_key = @router.find_node_key_by_key(key, primary: write)
109
119
  node = @router.find_node(node_key)
110
- yield ::RedisClient::Cluster::PinningNode.new(node)
120
+ node.with { |c| yield ::RedisClient::Cluster::PinningNode.new(c) }
111
121
  end
112
122
 
113
123
  def close
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.6
4
+ version: 0.7.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-17 00:00:00.000000000 Z
11
+ date: 2024-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -49,6 +49,7 @@ files:
49
49
  - lib/redis_client/cluster/node/random_replica_or_primary.rb
50
50
  - lib/redis_client/cluster/node_key.rb
51
51
  - lib/redis_client/cluster/normalized_cmd_name.rb
52
+ - lib/redis_client/cluster/optimistic_locking.rb
52
53
  - lib/redis_client/cluster/pinning_node.rb
53
54
  - lib/redis_client/cluster/pipeline.rb
54
55
  - lib/redis_client/cluster/pub_sub.rb