gracefully 0.0.1 → 0.1.0
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/.travis.yml +30 -0
- data/Gemfile +2 -0
- data/README.md +34 -1
- data/lib/gracefully.rb +22 -4
- data/lib/gracefully/all.rb +6 -0
- data/lib/gracefully/circuit_breaker.rb +88 -0
- data/lib/gracefully/command.rb +23 -0
- data/lib/gracefully/command_disabled_error.rb +6 -0
- data/lib/gracefully/consecutive_failures_based_health.rb +75 -0
- data/lib/gracefully/counter.rb +29 -0
- data/lib/gracefully/degradable.rb +36 -0
- data/lib/gracefully/{feature.rb → degradable_command.rb} +4 -3
- data/lib/gracefully/degradable_command_builder.rb +23 -0
- data/lib/gracefully/error.rb +37 -0
- data/lib/gracefully/health.rb +29 -0
- data/lib/gracefully/mutex_based_synchronized_counter.rb +30 -0
- data/lib/gracefully/retried_command.rb +25 -0
- data/lib/gracefully/short_circuited_command.rb +23 -0
- data/lib/gracefully/timed_command.rb +19 -0
- data/lib/gracefully/togglable_command.rb +20 -0
- data/lib/gracefully/try.rb +6 -34
- data/lib/gracefully/version.rb +1 -1
- data/spec/circuit_breaker_spec.rb +142 -0
- data/spec/command_spec.rb +34 -0
- data/spec/consecutive_failures_based_health_spec.rb +78 -0
- data/spec/degradable_spec.rb +49 -0
- data/spec/gracefully_spec.rb +136 -10
- data/spec/mutex_based_synchronized_counter_spec.rb +50 -0
- data/spec/retried_command_spec.rb +93 -0
- data/spec/short_circuited_command_spec.rb +136 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/timecop_helper.rb +3 -0
- data/spec/timed_command_spec.rb +73 -0
- data/spec/togglable_command_spec.rb +31 -0
- metadata +39 -5
- data/lib/gracefully/feature_builder.rb +0 -24
- data/lib/gracefully/health_meter.rb +0 -90
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'gracefully/degradable'
|
2
|
+
|
3
|
+
RSpec.describe Gracefully::Degradable do
|
4
|
+
describe 'an object included the Gracefully module' do
|
5
|
+
subject {
|
6
|
+
klass.new
|
7
|
+
}
|
8
|
+
|
9
|
+
context 'with the first method constantly failing' do
|
10
|
+
let(:klass) {
|
11
|
+
Class.new do
|
12
|
+
include Gracefully::Degradable
|
13
|
+
|
14
|
+
def foo
|
15
|
+
raise 'simulated error'
|
16
|
+
end
|
17
|
+
|
18
|
+
def bar
|
19
|
+
"bar"
|
20
|
+
end
|
21
|
+
|
22
|
+
gracefully_degrade :foo, fallback: [:bar]
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
specify { expect(subject.foo).to eq('bar') }
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with the first method timing out' do
|
30
|
+
let(:klass) {
|
31
|
+
Class.new do
|
32
|
+
include Gracefully::Degradable
|
33
|
+
|
34
|
+
def foo
|
35
|
+
sleep 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def baz
|
39
|
+
"baz"
|
40
|
+
end
|
41
|
+
|
42
|
+
gracefully_degrade :foo, timeout: 0.1, fallback: [:baz]
|
43
|
+
end
|
44
|
+
}
|
45
|
+
|
46
|
+
specify { expect(subject.foo).to eq('baz') }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/spec/gracefully_spec.rb
CHANGED
@@ -1,11 +1,22 @@
|
|
1
1
|
require 'gracefully'
|
2
2
|
|
3
|
+
RSpec.shared_context 'successful fallback' do
|
4
|
+
let(:fallback_to) {
|
5
|
+
-> arg1 { 'fallback:arg1:' + arg1 }
|
6
|
+
}
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.shared_context 'failing fallback' do
|
10
|
+
let(:fallback_to) {
|
11
|
+
-> arg1 { raise 'simulated error of fallback' }
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
3
15
|
RSpec.describe Gracefully do
|
4
|
-
describe "
|
16
|
+
describe "command call result" do
|
5
17
|
subject {
|
6
18
|
Gracefully.
|
7
|
-
|
8
|
-
usually(&usually).
|
19
|
+
degradable_command(&usually).
|
9
20
|
fallback_to(&fallback_to).
|
10
21
|
call input1
|
11
22
|
}
|
@@ -14,15 +25,13 @@ RSpec.describe Gracefully do
|
|
14
25
|
'input1'
|
15
26
|
}
|
16
27
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
context 'when the feature is defined for the name' do
|
22
|
-
let(:feature_name) {
|
23
|
-
:the_feature
|
28
|
+
context 'when the command is defined for the name' do
|
29
|
+
let(:command_name) {
|
30
|
+
:the_command
|
24
31
|
}
|
25
32
|
|
33
|
+
include_context 'successful fallback'
|
34
|
+
|
26
35
|
context 'when the usually block succeeds without any error' do
|
27
36
|
let(:usually) {
|
28
37
|
-> arg1 { 'usually:arg1:' + arg1 }
|
@@ -39,5 +48,122 @@ RSpec.describe Gracefully do
|
|
39
48
|
it { is_expected.to eq('fallback:arg1:input1') }
|
40
49
|
end
|
41
50
|
end
|
51
|
+
|
52
|
+
context 'when both the usual block and the fallback block fail' do
|
53
|
+
let(:command_name) {
|
54
|
+
:the_command
|
55
|
+
}
|
56
|
+
|
57
|
+
include_context 'failing fallback'
|
58
|
+
|
59
|
+
let(:usually) {
|
60
|
+
-> arg1 { raise 'simulated error' }
|
61
|
+
}
|
62
|
+
|
63
|
+
specify { expect { subject }.to raise_error(/Tried to get the value of a failure/) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'the command' do
|
68
|
+
let(:allowed_failures) { 1 }
|
69
|
+
|
70
|
+
let (:command) {
|
71
|
+
described_class.command(
|
72
|
+
timeout: 0.1,
|
73
|
+
retries: 1,
|
74
|
+
allowed_failures: allowed_failures,
|
75
|
+
run_only_if: run_only_if,
|
76
|
+
counter: -> { Gracefully::InMemoryCounter.new },
|
77
|
+
&body
|
78
|
+
)
|
79
|
+
}
|
80
|
+
|
81
|
+
subject {
|
82
|
+
command.call
|
83
|
+
}
|
84
|
+
|
85
|
+
context 'which is enabled' do
|
86
|
+
let(:run_only_if) {
|
87
|
+
-> { true }
|
88
|
+
}
|
89
|
+
|
90
|
+
context 'which is successful' do
|
91
|
+
let(:body) {
|
92
|
+
-> { 'ok' }
|
93
|
+
}
|
94
|
+
|
95
|
+
it { is_expected.to eq('ok') }
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'which fails at first and then succeeds' do
|
99
|
+
let(:body) {
|
100
|
+
count = 0
|
101
|
+
-> {
|
102
|
+
count += 1
|
103
|
+
if count == 1
|
104
|
+
raise 'simulated error'
|
105
|
+
else
|
106
|
+
'ok'
|
107
|
+
end
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
it { is_expected.to eq('ok') }
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'which is failing' do
|
115
|
+
let(:body) {
|
116
|
+
-> { raise 'simulated error' }
|
117
|
+
}
|
118
|
+
|
119
|
+
context 'after failures more than allowed' do
|
120
|
+
before do
|
121
|
+
(allowed_failures + 1).times do
|
122
|
+
expect { subject }.to raise_error(Gracefully::Error, 'simulated error')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
specify {
|
127
|
+
expect { subject }.to raise_error(Gracefully::CircuitBreaker::CurrentlyOpenError)
|
128
|
+
}
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'which is timing out' do
|
133
|
+
let(:body) {
|
134
|
+
-> { sleep 1 }
|
135
|
+
}
|
136
|
+
|
137
|
+
specify {
|
138
|
+
expect { subject }.to raise_error(Gracefully::Error, 'execution expired')
|
139
|
+
}
|
140
|
+
|
141
|
+
context 'after failures more than allowed' do
|
142
|
+
before do
|
143
|
+
(allowed_failures + 1).times do
|
144
|
+
expect { subject }.to raise_error(Gracefully::Error, 'execution expired')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
specify {
|
149
|
+
expect { subject }.to raise_error(Gracefully::CircuitBreaker::CurrentlyOpenError)
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'which is disabled' do
|
156
|
+
let(:run_only_if) {
|
157
|
+
-> { false }
|
158
|
+
}
|
159
|
+
|
160
|
+
let(:body) {
|
161
|
+
-> { 'ok' }
|
162
|
+
}
|
163
|
+
|
164
|
+
specify {
|
165
|
+
expect { subject }.to raise_error(Gracefully::CommandDisabledError)
|
166
|
+
}
|
167
|
+
end
|
42
168
|
end
|
43
169
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'gracefully/mutex_based_synchronized_counter'
|
2
|
+
|
3
|
+
RSpec.describe Gracefully::MutexBasedSynchronizedCounter do
|
4
|
+
subject {
|
5
|
+
counter.count
|
6
|
+
}
|
7
|
+
|
8
|
+
let(:counter) {
|
9
|
+
described_class.new(Gracefully::InMemoryCounter.new)
|
10
|
+
}
|
11
|
+
|
12
|
+
before do
|
13
|
+
@threads = 10.times.map do
|
14
|
+
Thread.abort_on_exception = true
|
15
|
+
Thread.start do
|
16
|
+
counter.increment!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
specify {
|
22
|
+
expect(subject).to be_between(0, 10)
|
23
|
+
}
|
24
|
+
|
25
|
+
context 'after all the threads have finished' do
|
26
|
+
before do
|
27
|
+
@threads.each(&:join)
|
28
|
+
end
|
29
|
+
|
30
|
+
it { is_expected.to eq(10) }
|
31
|
+
|
32
|
+
context 'and then reset' do
|
33
|
+
before do
|
34
|
+
@thread = Thread.start do
|
35
|
+
counter.reset!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it { is_expected.to eq(10).or eq(0) }
|
40
|
+
|
41
|
+
context 'eventually' do
|
42
|
+
before do
|
43
|
+
@thread.join
|
44
|
+
end
|
45
|
+
|
46
|
+
it { is_expected.to eq(0) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'gracefully'
|
2
|
+
require 'gracefully/command'
|
3
|
+
require 'gracefully/retried_command'
|
4
|
+
|
5
|
+
RSpec.shared_examples "a retried command" do
|
6
|
+
let(:input1) {
|
7
|
+
'input1'
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:callable) {
|
11
|
+
Gracefully::Command.new(&usually)
|
12
|
+
}
|
13
|
+
|
14
|
+
context 'when the block given to the command succeeds without any error' do
|
15
|
+
let(:usually) {
|
16
|
+
-> arg1 { 'usually:arg1:' + arg1 }
|
17
|
+
}
|
18
|
+
|
19
|
+
it { is_expected.to eq('usually:arg1:input1') }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when the usually block fails with an error' do
|
23
|
+
let(:usually) {
|
24
|
+
-> arg1 { raise 'simulated error' }
|
25
|
+
}
|
26
|
+
|
27
|
+
specify { expect { subject }.to raise_error('simulated error') }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when the usually block fails' do
|
31
|
+
let(:usually) {
|
32
|
+
num_trials = 0
|
33
|
+
-> arg1 {
|
34
|
+
num_trials += 1
|
35
|
+
if num_trials == 1
|
36
|
+
raise 'simulated error'
|
37
|
+
else
|
38
|
+
'usually:arg1:' + arg1
|
39
|
+
end
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
specify {
|
44
|
+
expect(subject).to eq('usually:arg1:input1')
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
RSpec.describe Gracefully::RetriedCommand do
|
50
|
+
context "command creation with invalid number of arguments" do
|
51
|
+
subject {
|
52
|
+
described_class.new(1, 2, 3)
|
53
|
+
}
|
54
|
+
|
55
|
+
specify { expect { subject }.to raise_error(/Invalid number of arguments: 3/) }
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'with 0 retries' do
|
59
|
+
context 'made of a callable object' do
|
60
|
+
subject {
|
61
|
+
described_class.new(usually, retries: 0).call input1
|
62
|
+
}
|
63
|
+
|
64
|
+
it_behaves_like 'a command'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "with more than 1 retries" do
|
69
|
+
context 'made of a callable object' do
|
70
|
+
subject {
|
71
|
+
described_class.new(usually, retries: 1).call input1
|
72
|
+
}
|
73
|
+
|
74
|
+
it_behaves_like 'a retried command'
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'made of a command' do
|
78
|
+
subject {
|
79
|
+
described_class.new(callable, retries: 1).call input1
|
80
|
+
}
|
81
|
+
|
82
|
+
it_behaves_like 'a retried command'
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'made of a block' do
|
86
|
+
subject {
|
87
|
+
described_class.new(retries: 1, &usually).call input1
|
88
|
+
}
|
89
|
+
|
90
|
+
it_behaves_like 'a retried command'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'gracefully'
|
2
|
+
require 'gracefully/short_circuited_command'
|
3
|
+
|
4
|
+
RSpec.shared_examples "a short-circuited command" do
|
5
|
+
let(:input1) {
|
6
|
+
'input1'
|
7
|
+
}
|
8
|
+
|
9
|
+
let(:callable) {
|
10
|
+
Gracefully::Command.new(&usually)
|
11
|
+
}
|
12
|
+
|
13
|
+
context 'when the block given to the command succeeds without any error' do
|
14
|
+
let(:usually) {
|
15
|
+
-> arg1 { 'usually:arg1:' + arg1 }
|
16
|
+
}
|
17
|
+
|
18
|
+
it { is_expected.to eq('usually:arg1:input1') }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when the usually block fails with an error' do
|
22
|
+
let(:usually) {
|
23
|
+
-> arg1 { raise 'simulated error' }
|
24
|
+
}
|
25
|
+
|
26
|
+
specify { expect { subject }.to raise_error('simulated error') }
|
27
|
+
|
28
|
+
context 'more than allowed times' do
|
29
|
+
let(:failed_at) {
|
30
|
+
Time.now
|
31
|
+
}
|
32
|
+
|
33
|
+
before do
|
34
|
+
Timecop.freeze(failed_at) do
|
35
|
+
2.times do
|
36
|
+
expect { subject }.to raise_error('simulated error')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
around do |ex|
|
42
|
+
Timecop.freeze(failed_at + passed_seconds) { ex.run }
|
43
|
+
end
|
44
|
+
|
45
|
+
context '0 seconds passed' do
|
46
|
+
let(:passed_seconds) { 0 }
|
47
|
+
|
48
|
+
specify {
|
49
|
+
expect { subject }.to raise_error(Gracefully::CircuitBreaker::CurrentlyOpenError)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'try_close_after seconds passed' do
|
54
|
+
let(:passed_seconds) { try_close_after }
|
55
|
+
|
56
|
+
specify {
|
57
|
+
expect { subject }.to raise_error(Gracefully::CircuitBreaker::CurrentlyOpenError)
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'try_close_after + 1 seconds passed' do
|
62
|
+
let(:passed_seconds) { try_close_after + 1 }
|
63
|
+
|
64
|
+
specify {
|
65
|
+
expect { subject }.to raise_error('simulated error')
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
RSpec.describe Gracefully::ShortCircuitedCommand do
|
73
|
+
context "command creation with invalid number of arguments" do
|
74
|
+
subject {
|
75
|
+
described_class.new(1, 2, 3)
|
76
|
+
}
|
77
|
+
|
78
|
+
specify { expect { subject }.to raise_error(/Invalid number of arguments: 3/) }
|
79
|
+
end
|
80
|
+
|
81
|
+
let(:try_close_after) {
|
82
|
+
3
|
83
|
+
}
|
84
|
+
|
85
|
+
describe 'with 0 allowed failures' do
|
86
|
+
context 'made of a callable object' do
|
87
|
+
subject {
|
88
|
+
described_class.new(usually, allowed_failures: 0).call input1
|
89
|
+
}
|
90
|
+
|
91
|
+
it_behaves_like 'a command'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "with more than 1 allowed failures" do
|
96
|
+
subject {
|
97
|
+
command.call input1
|
98
|
+
}
|
99
|
+
|
100
|
+
let(:allowed_failures) {
|
101
|
+
1
|
102
|
+
}
|
103
|
+
|
104
|
+
let(:options) {
|
105
|
+
{
|
106
|
+
allowed_failures: allowed_failures,
|
107
|
+
try_close_after: try_close_after,
|
108
|
+
counter: -> { Gracefully::InMemoryCounter.new }
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
context 'made of a callable object' do
|
113
|
+
let(:command) {
|
114
|
+
described_class.new(usually, options)
|
115
|
+
}
|
116
|
+
|
117
|
+
it_behaves_like 'a short-circuited command'
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'made of a command' do
|
121
|
+
let(:command) {
|
122
|
+
described_class.new(callable, options)
|
123
|
+
}
|
124
|
+
|
125
|
+
it_behaves_like 'a short-circuited command'
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'made of a block' do
|
129
|
+
let(:command) {
|
130
|
+
described_class.new(options, &usually)
|
131
|
+
}
|
132
|
+
|
133
|
+
it_behaves_like 'a short-circuited command'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|