circuit_breakage 0.1.0 → 0.1.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/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
|