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 +4 -4
- data/README.md +3 -0
- data/lib/redis-cluster.rb +26 -28
- data/lib/redis_cluster/circuit.rb +44 -0
- data/lib/redis_cluster/client.rb +30 -26
- data/lib/redis_cluster/cluster.rb +38 -33
- data/lib/redis_cluster/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2520c212f0cb77a948aef0fe024f39d00cdb4231723e8a308bcf5247c5e1c6ac
|
4
|
+
data.tar.gz: 20ce3bd10f0d341040e99bc64e685c885689ecb9d35c72c447f76a9f91897f10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 ||=
|
97
|
+
moved ||= error == :moved
|
98
|
+
down ||= error == :down
|
96
99
|
|
97
100
|
@pipeline.concat(leftover)
|
98
101
|
end
|
99
102
|
|
100
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
132
|
-
|
130
|
+
# make adjustment for cluster change
|
131
|
+
cluster.reset(force: true) if err == :moved
|
132
|
+
client = cluster[url]
|
133
133
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
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,
|
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
|
data/lib/redis_cluster/client.rb
CHANGED
@@ -6,25 +6,27 @@ require_relative 'version'
|
|
6
6
|
|
7
7
|
class RedisCluster
|
8
8
|
|
9
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
87
|
-
|
88
|
-
@
|
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
|
20
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
106
|
+
unhealthy_count = 0
|
93
107
|
|
94
108
|
(skip...pool.length).each do |i|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
116
|
+
buffer_length = pool.length - skip - unhealthy_count
|
117
|
+
return nil if buffer_length.zero?
|
102
118
|
|
103
|
-
i = rand(
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
143
|
-
|
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
|
-
|
161
|
+
reset(force: true)
|
157
162
|
end
|
158
163
|
|
159
164
|
def create_client(url)
|
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.
|
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:
|
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.
|
70
|
+
rubygems_version: 2.7.9
|
70
71
|
signing_key:
|
71
72
|
specification_version: 4
|
72
73
|
summary: Redis cluster client. Support pipelining.
|