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.
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