redis-cluster-client 0.7.6 → 0.7.8

Sign up to get free protection for your applications and to get access to all the features.
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