redis-cluster 1.0.1.pre.rc.1 → 1.1.0.pre.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|