redis-cluster 1.0.1.pre.rc.1 → 1.1.0.pre.rc.1

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: 9f8427709e6b67eaf1266d6e330dfc37958fbb10dc0ab3845ee97a14319466ce
4
- data.tar.gz: f26b971ccc78221ef3a3a1c6da1207e71d8b594d3c955ee529743abea5fb4c08
3
+ metadata.gz: 2520c212f0cb77a948aef0fe024f39d00cdb4231723e8a308bcf5247c5e1c6ac
4
+ data.tar.gz: 20ce3bd10f0d341040e99bc64e685c885689ecb9d35c72c447f76a9f91897f10
5
5
  SHA512:
6
- metadata.gz: bc342af53829102b0da2108ca999ca74473d62e54adeb3238e898f1e899a4510d5785b07c838b22a282dce5c65e59a8187f34cc3fc6595957dc36289d4595778
7
- data.tar.gz: 95221ec6964ab2aac0c639a4da59a0f501489e97ad5e27eb27be5f61a5c5158d96ae7c425eb9145b9360df06bd198b8510d96e8cbd2ed1226109ee98300bd6c9
6
+ metadata.gz: f64e6681e26e57b3a5d29244788f68b7ec45f847de472b0fecc5a5c5658621be096a5f03f6c6f5c145e249f6a25ddd9afc189dfd021aed9236ecca0e7fabfa6c
7
+ data.tar.gz: 576b6c1c881c10bf58b45aaaca92441e2985a5d8925b862eb854a18b4798046f2910778ae5284d3115184f532d003e73079e69be2bf302f935d2447d4e6fefe8
data/README.md CHANGED
@@ -94,6 +94,9 @@ Option for RedisCluster.
94
94
  - `read_mode`: for read command, RedisClient can try to read from slave if specified. Supported option is `:master`(default), `:slave`, and `:master_slave`.
95
95
  - `silent`: whether or not RedisCluster will raise error.
96
96
  - `logger`: if specified. RedisCluster will log all of RedisCluster errors here.
97
+ - `reset_interval`: reset threshold. A reset can only happen once per reset_interval.
98
+ - `circuit_threshold`: Threshold how many error that client will considered an unhealthy.
99
+ - `circuit_interval`: How long failed count will be remembered.
97
100
 
98
101
  #### Middlewares
99
102
 
data/lib/redis-cluster.rb CHANGED
@@ -4,6 +4,7 @@ require 'redis'
4
4
 
5
5
  require_relative 'redis_cluster/cluster'
6
6
  require_relative 'redis_cluster/client'
7
+ require_relative 'redis_cluster/circuit'
7
8
  require_relative 'redis_cluster/future'
8
9
  require_relative 'redis_cluster/function'
9
10
  require_relative 'redis_cluster/middlewares'
@@ -87,17 +88,20 @@ class RedisCluster
87
88
  while !@pipeline.empty? && try.positive?
88
89
  try -= 1
89
90
  moved = false
91
+ down = false
90
92
  mapped_future = map_pipeline(@pipeline)
91
93
 
92
94
  @pipeline = []
93
95
  mapped_future.each do |url, futures|
94
96
  leftover, error = do_pipelined(url, futures)
95
- moved ||= (error == :moved || error == :down)
97
+ moved ||= error == :moved
98
+ down ||= error == :down
96
99
 
97
100
  @pipeline.concat(leftover)
98
101
  end
99
102
 
100
- cluster.reset if moved
103
+ # force if moved, do not force if down
104
+ cluster.reset(force: moved) if moved || down
101
105
  end
102
106
 
103
107
  @pipeline.first.value unless @pipeline.empty?
@@ -115,36 +119,28 @@ class RedisCluster
115
119
  end
116
120
 
117
121
  def call_immediately(slot, command, transform:, read: false)
118
- try = 3
119
- asking = false
120
- reply = nil
121
122
  mode = read ? :read : :write
122
123
  client = cluster.client_for(mode, slot)
123
124
 
124
- while try.positive?
125
- begin
126
- try -= 1
127
-
128
- client.push([:asking]) if asking
129
- reply = client.call(command)
125
+ # first attempt
126
+ reply = client.call(command)
127
+ err, url = scan_reply(reply)
128
+ return transform.call(reply) unless err
130
129
 
131
- err, url = scan_reply(reply)
132
- return transform.call(reply) unless err
130
+ # make adjustment for cluster change
131
+ cluster.reset(force: true) if err == :moved
132
+ client = cluster[url]
133
133
 
134
- cluster.reset if err == :moved
135
- asking = err == :ask
136
- client = cluster[url]
137
- rescue NodeUnhealthyError, Redis::CannotConnectError => e
138
- if e.is_a?(Redis::CannotConnectError)
139
- asking = false
140
- cluster.reset
141
- end
142
- client = cluster.client_for(mode, slot)
143
- reply = e
144
- end
145
- end
134
+ # second attempt
135
+ client.push([:asking]) if err == :ask
136
+ reply = client.call(command)
137
+ err, = scan_reply(reply)
138
+ raise err if err
146
139
 
147
- raise reply
140
+ transform.call(reply)
141
+ rescue LoadingStateError, CircuitOpenError, Redis::BaseConnectionError => e
142
+ cluster.reset
143
+ raise e
148
144
  end
149
145
 
150
146
  def call_pipeline(slot, command, opts)
@@ -171,6 +167,7 @@ class RedisCluster
171
167
  idx = 0
172
168
  client = cluster[url]
173
169
 
170
+ # map reverse index for pipeline commands.
174
171
  futures.each_with_index do |future, i|
175
172
  if future.asking
176
173
  client.push([:asking])
@@ -198,11 +195,11 @@ class RedisCluster
198
195
  end
199
196
 
200
197
  [leftover, error]
201
- rescue NodeUnhealthyError, Redis::CannotConnectError => e
198
+ rescue LoadingStateError, CircuitOpenError, Redis::BaseConnectionError
202
199
  # reset url and asking when connection refused
203
200
  futures.each{ |f| f.url = nil; f.asking = false }
204
201
 
205
- [futures, e.is_a?(NodeUnhealthyError) ? :loading : :down]
202
+ [futures, :down]
206
203
  end
207
204
 
208
205
  def scan_reply(reply)
@@ -220,6 +217,7 @@ class RedisCluster
220
217
  host, port = url.split(':', 2)
221
218
  Client.new(redis_opts.merge(host: host, port: port)).tap do |c|
222
219
  c.middlewares = middlewares
220
+ c.circuit = Circuit.new(cluster_opts[:circuit_threshold].to_f, cluster_opts[:circuit_interval].to_f)
223
221
  end
224
222
  end
225
223
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisCluster
4
+
5
+ # Circuit is circuit breaker for RedisCluster.
6
+ class Circuit
7
+
8
+ attr_reader :fail_count, :ban_until
9
+
10
+ def initialize(threshold, interval)
11
+ @ban_until = Time.now
12
+ @fail_count = 0
13
+ @last_fail_time = Time.now
14
+ @fail_threshold = threshold
15
+ @interval_time = interval
16
+ end
17
+
18
+ # Failed is a method to add failed count and compare it to threshold,
19
+ # Will trip circuit if the count goes through threshold.
20
+ #
21
+ # @return[void]
22
+ def failed
23
+ @fail_count = 0 if @last_fail_time + (@interval_time * 1.5) < Time.now
24
+ @fail_count += 1
25
+ @last_fail_time = Time.now
26
+ open! if @fail_count >= @fail_threshold
27
+ end
28
+
29
+ # Open! is a method to update ban time.
30
+ #
31
+ # @return[void]
32
+ def open!
33
+ @ban_until = Time.now + @interval_time
34
+ end
35
+
36
+ # Open? is a method to check if the circuit breaker status.
37
+ #
38
+ # @return[Boolean] Wheter the circuit is open or not
39
+ def open?
40
+ @ban_until > Time.now
41
+ end
42
+
43
+ end
44
+ end
@@ -6,25 +6,27 @@ require_relative 'version'
6
6
 
7
7
  class RedisCluster
8
8
 
9
- class NodeUnhealthyError < StandardError; end
9
+ # LoadingStateError is an error when try to read redis that in loading state.
10
+ class LoadingStateError < StandardError; end
11
+
12
+ # CircuitOpenError is an error that fired when circuit in client is trip.
13
+ class CircuitOpenError < StandardError; end
10
14
 
11
15
  # Client is a decorator object for Redis::Client. It add queue to support pipelining and another
12
16
  # useful addition
13
17
  class Client
14
18
  attr_reader :client, :queue, :url
15
- attr_accessor :middlewares
19
+ attr_accessor :middlewares, :circuit, :role, :refresh
16
20
 
17
21
  def initialize(opts)
18
22
  @client = Redis::Client.new(opts)
19
23
  @queue = []
20
24
  @url = "#{client.host}:#{client.port}"
21
-
22
- @healthy = true
23
- @ban_from = nil
25
+ @ready = false
24
26
  end
25
27
 
26
28
  def inspect
27
- "#<RedisCluster client v#{RedisCluster::VERSION} for #{url}>"
29
+ "#<RedisCluster client v#{RedisCluster::VERSION} for #{url} (#{role} at #{refresh}) status #{healthy? ? 'healthy' : 'unhealthy'}>"
28
30
  end
29
31
 
30
32
  def connected?
@@ -52,21 +54,23 @@ class RedisCluster
52
54
  end
53
55
  end
54
56
 
57
+ # Healthy? will retrun true if circuit breaker is not open and Redis Client is ready.
58
+ # Redis Client will be ready if it already sent with `readonly` command.
59
+ #
60
+ # return [Boolean] Whether client is healthy or not.
55
61
  def healthy?
56
- return true if @healthy
57
-
58
- # ban for 60 seconds for unhealthy state
59
- if Time.now - @ban_from > 60
60
- @healthy = true
61
- @ban_from = nil
62
- end
62
+ return false if @circuit.open?
63
+ prepare unless @ready
63
64
 
64
- @healthy
65
+ true
66
+ rescue StandardError
67
+ false
65
68
  end
66
69
 
67
70
  private
68
71
 
69
72
  def _commit
73
+ raise CircuitOpenError, "Circuit open in client #{url} until #{@circuit.ban_until}" if @circuit.open?
70
74
  return nil if queue.empty?
71
75
 
72
76
  result = Array.new(queue.size)
@@ -74,26 +78,26 @@ class RedisCluster
74
78
  queue.size.times do |i|
75
79
  result[i] = client.read
76
80
 
77
- unhealthy!(result[i]) if error?(result[i])
81
+ if result[i].is_a?(Redis::CommandError) && result[i].message['LOADING']
82
+ @circuit.open!
83
+ raise LoadingStateError, "Client #{url} is in Loading State"
84
+ end
78
85
  end
79
86
  end
80
87
 
81
88
  result
89
+ rescue LoadingStateError, CircuitOpenError, Redis::BaseConnectionError => e
90
+ @circuit.failed
91
+ @ready = false if @circuit.open?
92
+
93
+ [e] # return this
82
94
  ensure
83
95
  @queue = []
84
96
  end
85
97
 
86
- def unhealthy!(cause)
87
- @healthy = false
88
- @ban_from = Time.now
89
-
90
- error = NodeUnhealthyError.new("Node #{@url} is unhealthy: #{cause}")
91
- error.set_backtrace(cause.backtrace)
92
- raise error
93
- end
94
-
95
- def error?(res)
96
- res.is_a?(Redis::CommandError) && (res.message['LOADING'] || res.message['CLUSTERDOWN'])
98
+ def prepare
99
+ call([:readonly])
100
+ @ready = true
97
101
  end
98
102
  end
99
103
  end
@@ -16,10 +16,8 @@ class RedisCluster
16
16
  @slots = []
17
17
  @clients = {}
18
18
  @replicas = nil
19
- @client_creater = block || proc do |url|
20
- host, port = url.split(':', 2)
21
- Client.new(host: host, port: port)
22
- end
19
+ @client_creater = block
20
+ @last_reset = Time.now - reset_interval
23
21
 
24
22
  @buffer = []
25
23
  init_client(seeds)
@@ -33,8 +31,11 @@ class RedisCluster
33
31
  options[:read_mode] || :master
34
32
  end
35
33
 
36
- def logger
37
- options[:logger]
34
+ # Reset_interval return interval in second which reset can happen. A reset can only happen once per reset_interval.
35
+ #
36
+ # @return [Fixnum] reset interval
37
+ def reset_interval
38
+ options[:reset_interval].to_f
38
39
  end
39
40
 
40
41
  def slot_for(keys)
@@ -67,15 +68,28 @@ class RedisCluster
67
68
  clients.values.sample
68
69
  end
69
70
 
70
- def reset
71
+ # Reset will reload cluster topology. Reset will only be executed once per reset_interval if not forced.
72
+ #
73
+ # @param [Boolean] force: Whether to force reset to happen or not.
74
+ # @return [void]
75
+ def reset(force: false)
76
+ return if !force && @last_reset + reset_interval > Time.now
77
+
71
78
  try = 3
79
+ # binding.pry
80
+ pool = clients.values.select(&:healthy?)
72
81
  begin
73
82
  try -= 1
74
- client = random
83
+ raise 'No healthy seed' if pool.length.zero?
84
+
85
+ i = rand(pool.length)
86
+ client = pool[i]
75
87
  slots_and_clients(client)
76
88
  rescue StandardError => e
89
+ pool.delete_at(i)
77
90
  try.positive? ? retry : (raise e)
78
91
  end
92
+ @last_reset = Time.now
79
93
  end
80
94
 
81
95
  def [](url)
@@ -89,18 +103,20 @@ class RedisCluster
89
103
  private
90
104
 
91
105
  def pick_client(pool, skip: 0)
92
- buff_len = 0
106
+ unhealthy_count = 0
93
107
 
94
108
  (skip...pool.length).each do |i|
95
- next unless pool[i].healthy?
96
-
97
- @buffer[buff_len] = pool[i]
98
- buff_len += 1
109
+ if pool[i].healthy?
110
+ @buffer[i - skip] = pool[i]
111
+ else
112
+ unhealthy_count += 1
113
+ end
99
114
  end
100
115
 
101
- return nil if buff_len.zero?
116
+ buffer_length = pool.length - skip - unhealthy_count
117
+ return nil if buffer_length.zero?
102
118
 
103
- i = rand(buff_len)
119
+ i = rand(buffer_length)
104
120
  @buffer[i]
105
121
  end
106
122
 
@@ -122,11 +138,9 @@ class RedisCluster
122
138
  arr[2..-1].each_with_index do |a, i|
123
139
  cli = self["#{a[0]}:#{a[1]}"]
124
140
  replicas[arr[0]] << cli
125
- begin
126
- cli.call([:readonly]) if i.nonzero?
127
- rescue NodeUnhealthyError => e
128
- logger&.error(e)
129
- end
141
+
142
+ cli.role = i.zero? ? :master : :slave
143
+ cli.refresh = Time.now
130
144
  end
131
145
 
132
146
  (arr[0]..arr[1]).each do |slot|
@@ -139,21 +153,12 @@ class RedisCluster
139
153
  end
140
154
 
141
155
  def init_client(seeds)
142
- try = seeds.count
143
- err = nil
144
-
145
- while try.positive?
146
- try -= 1
147
- begin
148
- client = create_client(seeds[try])
149
- slots_and_clients(client)
150
- return
151
- rescue StandardError => e
152
- err = e
153
- end
156
+ # register seeds into clients
157
+ seeds.each do |s|
158
+ self[s]
154
159
  end
155
160
 
156
- raise err
161
+ reset(force: true)
157
162
  end
158
163
 
159
164
  def create_client(url)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisCluster
4
- VERSION = '1.0.1-rc.1'
4
+ VERSION = '1.1.0-rc.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1.pre.rc.1
4
+ version: 1.1.0.pre.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bukalapak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-25 00:00:00.000000000 Z
11
+ date: 2019-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -33,6 +33,7 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - README.md
35
35
  - lib/redis-cluster.rb
36
+ - lib/redis_cluster/circuit.rb
36
37
  - lib/redis_cluster/client.rb
37
38
  - lib/redis_cluster/cluster.rb
38
39
  - lib/redis_cluster/function.rb
@@ -66,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
67
  version: 1.3.1
67
68
  requirements: []
68
69
  rubyforge_project:
69
- rubygems_version: 2.7.7
70
+ rubygems_version: 2.7.9
70
71
  signing_key:
71
72
  specification_version: 4
72
73
  summary: Redis cluster client. Support pipelining.