gracefully 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +30 -0
  3. data/Gemfile +2 -0
  4. data/README.md +34 -1
  5. data/lib/gracefully.rb +22 -4
  6. data/lib/gracefully/all.rb +6 -0
  7. data/lib/gracefully/circuit_breaker.rb +88 -0
  8. data/lib/gracefully/command.rb +23 -0
  9. data/lib/gracefully/command_disabled_error.rb +6 -0
  10. data/lib/gracefully/consecutive_failures_based_health.rb +75 -0
  11. data/lib/gracefully/counter.rb +29 -0
  12. data/lib/gracefully/degradable.rb +36 -0
  13. data/lib/gracefully/{feature.rb → degradable_command.rb} +4 -3
  14. data/lib/gracefully/degradable_command_builder.rb +23 -0
  15. data/lib/gracefully/error.rb +37 -0
  16. data/lib/gracefully/health.rb +29 -0
  17. data/lib/gracefully/mutex_based_synchronized_counter.rb +30 -0
  18. data/lib/gracefully/retried_command.rb +25 -0
  19. data/lib/gracefully/short_circuited_command.rb +23 -0
  20. data/lib/gracefully/timed_command.rb +19 -0
  21. data/lib/gracefully/togglable_command.rb +20 -0
  22. data/lib/gracefully/try.rb +6 -34
  23. data/lib/gracefully/version.rb +1 -1
  24. data/spec/circuit_breaker_spec.rb +142 -0
  25. data/spec/command_spec.rb +34 -0
  26. data/spec/consecutive_failures_based_health_spec.rb +78 -0
  27. data/spec/degradable_spec.rb +49 -0
  28. data/spec/gracefully_spec.rb +136 -10
  29. data/spec/mutex_based_synchronized_counter_spec.rb +50 -0
  30. data/spec/retried_command_spec.rb +93 -0
  31. data/spec/short_circuited_command_spec.rb +136 -0
  32. data/spec/spec_helper.rb +3 -0
  33. data/spec/timecop_helper.rb +3 -0
  34. data/spec/timed_command_spec.rb +73 -0
  35. data/spec/togglable_command_spec.rb +31 -0
  36. metadata +39 -5
  37. data/lib/gracefully/feature_builder.rb +0 -24
  38. 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
@@ -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 "feature call result" do
16
+ describe "command call result" do
5
17
  subject {
6
18
  Gracefully.
7
- degrade(feature_name).
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
- let(:fallback_to) {
18
- -> arg1 { 'fallback:arg1:' + arg1 }
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