circuit_breakage 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/circuit_breakage/redis_backed_breaker.rb +16 -18
- data/lib/circuit_breakage/version.rb +1 -1
- data/spec/breaker_spec.rb +3 -55
- data/spec/redis_backed_breaker_spec.rb +30 -61
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 264f3254581c4bdf631cfb997e2a66a177a19d92
|
4
|
+
data.tar.gz: 59a4cca4490e79cd3195c9a04b5f1a07a85c5e9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64348b4b18f1f35e66a1c1b45993093b36cb31ea6ddd5b2e814f32898dfe1de1e869c976a777bea536e7efea2a9051734676821b85221b5c014c1e266f01ad72
|
7
|
+
data.tar.gz: ae0ecdcedc18b541b266b6cf800a6710c487c24549f8bbbb56f8a75e8e2b56d4a9ce42e8591392ed43991b15df2e8b9b9faa6b0a6bb1935d16ba292508f2db68
|
@@ -11,13 +11,23 @@ module CircuitBreakage
|
|
11
11
|
attr_reader :connection, :key
|
12
12
|
|
13
13
|
def initialize(connection, key, block)
|
14
|
-
raise NotImplementedError.new("Still working on it!")
|
15
|
-
|
16
14
|
@connection = connection
|
17
15
|
@key = key
|
18
16
|
super(block)
|
19
17
|
end
|
20
18
|
|
19
|
+
[:state, :failure_count, :last_failed].each do |attr|
|
20
|
+
attr_key = "#{@key}/attrs/#{attr}"
|
21
|
+
|
22
|
+
define_method(attr) do
|
23
|
+
@connection.get(attr_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method("#{attr}=") do |value|
|
27
|
+
@connection.set(attr_key, value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
21
31
|
private
|
22
32
|
|
23
33
|
def do_retry(*args)
|
@@ -30,13 +40,13 @@ module CircuitBreakage
|
|
30
40
|
mutex_key = "#{@key}/locks/#{lock}"
|
31
41
|
|
32
42
|
acquired = @connection.setnx(mutex_key, Time.now.to_i)
|
33
|
-
if acquired
|
43
|
+
if acquired == 0 # mutex is already acquired
|
34
44
|
locked_at = @connection.get(mutex_key)
|
35
|
-
|
45
|
+
raise CircuitBreakage::CircuitOpen if locked_at + LOCK_TIMEOUT < Time.now.to_i
|
36
46
|
locked_at_second_check = @connection.getset(mutex_key, Time.now.to_i)
|
37
|
-
|
47
|
+
raise CircuitBreakage::CircuitOpen if locked_at_second_check != locked_at
|
48
|
+
# If we get here, then the lock is expired, and we're the first to re-acquire it.
|
38
49
|
end
|
39
|
-
# If we get this far, we have successfully acquired the mutex.
|
40
50
|
|
41
51
|
begin
|
42
52
|
block.call
|
@@ -45,17 +55,5 @@ module CircuitBreakage
|
|
45
55
|
end
|
46
56
|
end
|
47
57
|
|
48
|
-
[:state, :failure_count, :last_failed].each do |attr|
|
49
|
-
attr_key = "#{@key}/attrs/#{attr}"
|
50
|
-
|
51
|
-
define_method(attr) do
|
52
|
-
@connection.get(attr_key)
|
53
|
-
end
|
54
|
-
|
55
|
-
define_method("#{attr}=") do |value|
|
56
|
-
@connection.set(attr_key, value)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
58
|
end
|
61
59
|
end
|
data/spec/breaker_spec.rb
CHANGED
@@ -14,7 +14,7 @@ module CircuitBreakage
|
|
14
14
|
let(:arg) { 'This is an argument.' }
|
15
15
|
|
16
16
|
context 'when the circuit is closed' do
|
17
|
-
before { breaker.closed
|
17
|
+
before { breaker.state = 'closed' }
|
18
18
|
|
19
19
|
it 'calls the block' do
|
20
20
|
# The default block just returns the arg.
|
@@ -37,7 +37,7 @@ module CircuitBreakage
|
|
37
37
|
context 'and the failure count exceeds the failure threshold' do
|
38
38
|
before { breaker.failure_count = breaker.failure_threshold }
|
39
39
|
|
40
|
-
it { is_expected.to change { breaker.
|
40
|
+
it { is_expected.to change { breaker.state }.to('open') }
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -52,7 +52,7 @@ module CircuitBreakage
|
|
52
52
|
end
|
53
53
|
|
54
54
|
context 'when the circuit is open' do
|
55
|
-
before { breaker.open
|
55
|
+
before { breaker.state = 'open' }
|
56
56
|
|
57
57
|
context 'before the retry_time' do
|
58
58
|
before { breaker.last_failed = Time.now - breaker.duration + 30 }
|
@@ -64,62 +64,10 @@ module CircuitBreakage
|
|
64
64
|
before { breaker.last_failed = Time.now - breaker.duration - 30 }
|
65
65
|
|
66
66
|
it 'calls the block' do
|
67
|
-
# This is the same as being half open, see below for further tests.
|
68
67
|
expect(breaker.call(arg)).to eq arg
|
69
68
|
end
|
70
69
|
end
|
71
70
|
end
|
72
|
-
|
73
|
-
context 'when the circuit is half open' do
|
74
|
-
before do
|
75
|
-
# For the circuit to be tripped in the first place, the failure count
|
76
|
-
# must have reached the failure threshold.
|
77
|
-
breaker.failure_count = breaker.failure_threshold
|
78
|
-
breaker.half_open!
|
79
|
-
end
|
80
|
-
|
81
|
-
it 'calls the block' do
|
82
|
-
expect(breaker.call(arg)).to eq arg
|
83
|
-
end
|
84
|
-
|
85
|
-
context 'and the call succeeds' do
|
86
|
-
before { breaker.failure_count = 3 }
|
87
|
-
|
88
|
-
it { is_expected.to change { breaker.closed? }.to(true) }
|
89
|
-
it { is_expected.to change { breaker.failure_count }.to(0) }
|
90
|
-
end
|
91
|
-
|
92
|
-
context 'and the call fails' do
|
93
|
-
let(:block) { -> { raise 'some error' } }
|
94
|
-
|
95
|
-
it { is_expected.to change { breaker.open? }.to(true) }
|
96
|
-
it { is_expected.to change { breaker.last_failed } }
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
# #half_open?, #half_open!, #closed?, and #closed! are all exactly the same
|
102
|
-
# as #open? and #open!, so we're just going to test the open methods.
|
103
|
-
|
104
|
-
describe '#open!' do
|
105
|
-
it 'opens the circuit' do
|
106
|
-
breaker.open!
|
107
|
-
expect(breaker).to be_open
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
describe '#open?' do
|
112
|
-
subject { breaker.open? }
|
113
|
-
|
114
|
-
context 'when open' do
|
115
|
-
before { breaker.open! }
|
116
|
-
it { is_expected.to be_truthy }
|
117
|
-
end
|
118
|
-
|
119
|
-
context 'when not open' do
|
120
|
-
before { breaker.closed! }
|
121
|
-
it { is_expected.to be_falsey }
|
122
|
-
end
|
123
71
|
end
|
124
72
|
end
|
125
73
|
end
|
@@ -4,7 +4,7 @@
|
|
4
4
|
module CircuitBreakage
|
5
5
|
describe RedisBackedBreaker do
|
6
6
|
let(:breaker) { RedisBackedBreaker.new(connection, key, block) }
|
7
|
-
let(:connection) {
|
7
|
+
let(:connection) { MockRedis.new }
|
8
8
|
let(:key) { 'test/data' }
|
9
9
|
let(:block) { ->(x) { return x } }
|
10
10
|
|
@@ -13,7 +13,7 @@ module CircuitBreakage
|
|
13
13
|
let(:arg) { 'This is an argument.' }
|
14
14
|
|
15
15
|
context 'when the circuit is closed' do
|
16
|
-
before { breaker.closed
|
16
|
+
before { breaker.state = 'closed' }
|
17
17
|
|
18
18
|
it 'calls the block' do
|
19
19
|
# The default block just returns the arg.
|
@@ -36,7 +36,7 @@ module CircuitBreakage
|
|
36
36
|
context 'and the failure count exceeds the failure threshold' do
|
37
37
|
before { breaker.failure_count = breaker.failure_threshold }
|
38
38
|
|
39
|
-
it { is_expected.to change { breaker.
|
39
|
+
it { is_expected.to change { breaker.state }.to('open') }
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -51,7 +51,7 @@ module CircuitBreakage
|
|
51
51
|
end
|
52
52
|
|
53
53
|
context 'when the circuit is open' do
|
54
|
-
before { breaker.open
|
54
|
+
before { breaker.state = 'open' }
|
55
55
|
|
56
56
|
context 'before the retry_time' do
|
57
57
|
before { breaker.last_failed = Time.now - breaker.duration + 30 }
|
@@ -60,81 +60,50 @@ module CircuitBreakage
|
|
60
60
|
end
|
61
61
|
|
62
62
|
context 'after the retry time' do
|
63
|
-
before { breaker.last_failed = Time.now - breaker.duration - 30 }
|
64
|
-
|
65
63
|
it 'calls the block' do
|
66
|
-
|
64
|
+
breaker.last_failed = Time.at(0)
|
67
65
|
expect(breaker.call(arg)).to eq arg
|
68
66
|
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
context 'when the circuit is half open' do
|
73
|
-
before do
|
74
|
-
# For the circuit to be tripped in the first place, the failure count
|
75
|
-
# must have reached the failure threshold.
|
76
|
-
breaker.failure_count = breaker.failure_threshold
|
77
|
-
breaker.half_open!
|
78
|
-
end
|
79
67
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
context 'and the call succeeds' do
|
85
|
-
before { breaker.failure_count = 3 }
|
86
|
-
|
87
|
-
it { is_expected.to change { breaker.closed? }.to(true) }
|
88
|
-
it { is_expected.to change { breaker.failure_count }.to(0) }
|
89
|
-
end
|
90
|
-
|
91
|
-
context 'and the call fails' do
|
92
|
-
let(:block) { -> { raise 'some error' } }
|
93
|
-
|
94
|
-
it { is_expected.to change { breaker.open? }.to(true) }
|
95
|
-
it { is_expected.to change { breaker.last_failed } }
|
68
|
+
# TODO: It would be nice to test some of the concurrency scenarios,
|
69
|
+
# but that's pretty tricky to do politely in a test suite.
|
96
70
|
end
|
97
71
|
end
|
98
72
|
end
|
99
|
-
|
100
|
-
# #half_open?, #half_open!, #closed?, and #closed! are all exactly the same
|
101
|
-
# as #open? and #open!, so we're just going to test the open methods.
|
102
|
-
|
103
|
-
describe '#open!' do
|
104
|
-
it 'opens the circuit' do
|
105
|
-
breaker.open!
|
106
|
-
expect(breaker).to be_open
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
describe '#open?' do
|
111
|
-
subject { breaker.open? }
|
112
|
-
|
113
|
-
context 'when open' do
|
114
|
-
before { breaker.open! }
|
115
|
-
it { is_expected.to be_truthy }
|
116
|
-
end
|
117
|
-
|
118
|
-
context 'when not open' do
|
119
|
-
before { breaker.closed! }
|
120
|
-
it { is_expected.to be_falsey }
|
121
|
-
end
|
122
|
-
end
|
123
73
|
end
|
124
74
|
end
|
125
75
|
|
126
|
-
class
|
76
|
+
class MockRedis
|
127
77
|
attr_accessor :stored_data
|
128
78
|
|
129
79
|
def initialize
|
130
80
|
@stored_data = {}
|
131
81
|
end
|
132
82
|
|
133
|
-
def
|
83
|
+
def get(key)
|
84
|
+
@stored_data[key]
|
85
|
+
end
|
86
|
+
|
87
|
+
def set(key, val)
|
134
88
|
@stored_data[key] = val
|
135
89
|
end
|
136
90
|
|
137
|
-
def
|
138
|
-
@stored_data[key]
|
91
|
+
def setnx(key, val)
|
92
|
+
if @stored_data[key].nil?
|
93
|
+
@stored_data[key] = val
|
94
|
+
return 1
|
95
|
+
else
|
96
|
+
return 0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def getset(key, val)
|
101
|
+
old_val = @stored_data[key]
|
102
|
+
@stored_data[key] = val
|
103
|
+
return old_val
|
104
|
+
end
|
105
|
+
|
106
|
+
def del(key)
|
107
|
+
@stored_data.delete(key)
|
139
108
|
end
|
140
109
|
end
|