stoplight 4.1.1 → 5.0.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +288 -354
  3. data/lib/stoplight/admin/actions/action.rb +24 -0
  4. data/lib/stoplight/admin/actions/lock.rb +23 -0
  5. data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
  6. data/lib/stoplight/admin/actions/lock_green.rb +23 -0
  7. data/lib/stoplight/admin/actions/lock_red.rb +23 -0
  8. data/lib/stoplight/admin/actions/stats.rb +27 -0
  9. data/lib/stoplight/admin/actions/unlock.rb +23 -0
  10. data/lib/stoplight/admin/dependencies.rb +50 -0
  11. data/lib/stoplight/admin/helpers.rb +27 -0
  12. data/lib/stoplight/admin/lights_repository/light.rb +155 -0
  13. data/lib/stoplight/admin/lights_repository.rb +74 -0
  14. data/lib/stoplight/admin/lights_stats.rb +77 -0
  15. data/lib/stoplight/admin/views/_card.erb +120 -0
  16. data/lib/stoplight/admin/views/index.erb +36 -0
  17. data/lib/stoplight/admin/views/layout.erb +66 -0
  18. data/lib/stoplight/admin.rb +68 -0
  19. data/lib/stoplight/color.rb +3 -3
  20. data/lib/stoplight/config/config_provider.rb +62 -0
  21. data/lib/stoplight/config/library_default_config.rb +29 -0
  22. data/lib/stoplight/config/user_default_config.rb +83 -0
  23. data/lib/stoplight/data_store/base.rb +59 -33
  24. data/lib/stoplight/data_store/fail_safe.rb +105 -0
  25. data/lib/stoplight/data_store/memory.rb +257 -50
  26. data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
  27. data/lib/stoplight/data_store/redis/lua.rb +23 -0
  28. data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
  29. data/lib/stoplight/data_store/redis/record_success.lua +35 -0
  30. data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
  31. data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
  32. data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
  33. data/lib/stoplight/data_store/redis.rb +345 -106
  34. data/lib/stoplight/default.rb +11 -9
  35. data/lib/stoplight/error.rb +1 -13
  36. data/lib/stoplight/failure.rb +14 -13
  37. data/lib/stoplight/light/config.rb +118 -0
  38. data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
  39. data/lib/stoplight/light/green_run_strategy.rb +53 -0
  40. data/lib/stoplight/light/red_run_strategy.rb +26 -0
  41. data/lib/stoplight/light/run_strategy.rb +30 -0
  42. data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
  43. data/lib/stoplight/light.rb +164 -84
  44. data/lib/stoplight/metadata.rb +71 -0
  45. data/lib/stoplight/notifier/base.rb +14 -7
  46. data/lib/stoplight/notifier/fail_safe.rb +67 -0
  47. data/lib/stoplight/notifier/generic.rb +54 -5
  48. data/lib/stoplight/rspec/generic_notifier.rb +11 -12
  49. data/lib/stoplight/rspec.rb +1 -1
  50. data/lib/stoplight/state.rb +3 -3
  51. data/lib/stoplight/traffic_control/base.rb +35 -0
  52. data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
  53. data/lib/stoplight/traffic_recovery/base.rb +51 -0
  54. data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
  55. data/lib/stoplight/version.rb +1 -1
  56. data/lib/stoplight.rb +111 -51
  57. metadata +49 -98
  58. data/lib/stoplight/builder.rb +0 -70
  59. data/lib/stoplight/circuit_breaker.rb +0 -102
  60. data/lib/stoplight/configurable.rb +0 -95
  61. data/lib/stoplight/configuration.rb +0 -126
  62. data/lib/stoplight/light/deprecated.rb +0 -44
  63. data/lib/stoplight/light/lockable.rb +0 -45
  64. data/lib/stoplight/light/runnable.rb +0 -127
  65. data/lib/stoplight/notifier.rb +0 -6
  66. data/spec/spec_helper.rb +0 -22
  67. data/spec/stoplight/builder_spec.rb +0 -165
  68. data/spec/stoplight/circuit_breaker_spec.rb +0 -43
  69. data/spec/stoplight/color_spec.rb +0 -39
  70. data/spec/stoplight/configurable_spec.rb +0 -25
  71. data/spec/stoplight/data_store/base_spec.rb +0 -71
  72. data/spec/stoplight/data_store/memory_spec.rb +0 -22
  73. data/spec/stoplight/data_store/redis_spec.rb +0 -45
  74. data/spec/stoplight/data_store_spec.rb +0 -9
  75. data/spec/stoplight/default_spec.rb +0 -80
  76. data/spec/stoplight/error_spec.rb +0 -39
  77. data/spec/stoplight/failure_spec.rb +0 -108
  78. data/spec/stoplight/light/lockable_spec.rb +0 -93
  79. data/spec/stoplight/light/runnable_spec.rb +0 -38
  80. data/spec/stoplight/light_spec.rb +0 -156
  81. data/spec/stoplight/notifier/base_spec.rb +0 -18
  82. data/spec/stoplight/notifier/generic_spec.rb +0 -50
  83. data/spec/stoplight/notifier/io_spec.rb +0 -41
  84. data/spec/stoplight/notifier/logger_spec.rb +0 -75
  85. data/spec/stoplight/notifier_spec.rb +0 -9
  86. data/spec/stoplight/state_spec.rb +0 -39
  87. data/spec/stoplight/version_spec.rb +0 -9
  88. data/spec/stoplight_spec.rb +0 -32
  89. data/spec/support/configurable.rb +0 -69
  90. data/spec/support/data_store/base/clear_failures.rb +0 -24
  91. data/spec/support/data_store/base/clear_state.rb +0 -20
  92. data/spec/support/data_store/base/get_all.rb +0 -44
  93. data/spec/support/data_store/base/get_failures.rb +0 -30
  94. data/spec/support/data_store/base/get_state.rb +0 -7
  95. data/spec/support/data_store/base/names.rb +0 -29
  96. data/spec/support/data_store/base/record_failures.rb +0 -70
  97. data/spec/support/data_store/base/set_state.rb +0 -15
  98. data/spec/support/data_store/base/with_notification_lock.rb +0 -27
  99. data/spec/support/data_store/base.rb +0 -21
  100. data/spec/support/database_cleaner.rb +0 -26
  101. data/spec/support/exception_helpers.rb +0 -9
  102. data/spec/support/light/runnable/color.rb +0 -79
  103. data/spec/support/light/runnable/run.rb +0 -247
  104. data/spec/support/light/runnable/state.rb +0 -31
  105. data/spec/support/light/runnable.rb +0 -5
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::DataStore::Base#record_failure' do
4
- it 'returns the number of failures' do
5
- expect(data_store.record_failure(light, failure)).to eql(1)
6
- end
7
-
8
- context 'when there is an error' do
9
- before do
10
- data_store.record_failure(light, failure)
11
- end
12
-
13
- it 'persists the failure' do
14
- expect(data_store.get_failures(light)).to eq([failure])
15
- end
16
- end
17
-
18
- context 'when there is are several errors' do
19
- before do
20
- data_store.record_failure(light, failure)
21
- data_store.record_failure(light, other)
22
- end
23
-
24
- it 'stores more recent failures at the head' do
25
- expect(data_store.get_failures(light)).to eq([other, failure])
26
- end
27
- end
28
-
29
- shared_examples 'with_threshold' do
30
- context 'when the number of errors is bigger then threshold' do
31
- before do
32
- light.with_threshold(1)
33
-
34
- data_store.record_failure(light, failure)
35
- end
36
-
37
- it 'limits the number of stored failures' do
38
- expect do
39
- data_store.record_failure(light, other)
40
- end.to change { data_store.get_failures(light) }
41
- .from([failure])
42
- .to([other])
43
- end
44
- end
45
- end
46
-
47
- it_behaves_like 'with_threshold'
48
-
49
- context 'with window_size' do
50
- let(:window_size) { 3600 }
51
-
52
- before do
53
- light.with_window_size(window_size)
54
- end
55
-
56
- context 'when error is outside of the window' do
57
- let(:older_failure) { Stoplight::Failure.new('class', 'message 3', Time.new - window_size - 1) }
58
-
59
- it 'stores failures only withing window length' do
60
- data_store.record_failure(light, failure)
61
- data_store.record_failure(light, other)
62
- data_store.record_failure(light, older_failure)
63
-
64
- expect(data_store.get_failures(light)).to eq([other, failure])
65
- end
66
- end
67
-
68
- it_behaves_like 'with_threshold'
69
- end
70
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::DataStore::Base#set_state' do
4
- let(:state) { 'state' }
5
-
6
- it 'returns the state' do
7
- expect(data_store.set_state(light, state)).to eql(state)
8
- end
9
-
10
- it 'persists the state' do
11
- data_store.set_state(light, state)
12
-
13
- expect(data_store.get_state(light)).to eql(state)
14
- end
15
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::DataStore::Base#with_notification_lock' do
4
- context 'when notification is already sent' do
5
- before do
6
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
7
- end
8
-
9
- it 'does not yield passed block' do
10
- expect do |b|
11
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED, &b)
12
- end.not_to yield_control
13
- end
14
- end
15
-
16
- context 'when notification is not already sent' do
17
- before do
18
- data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
19
- end
20
-
21
- it 'yields passed block' do
22
- expect do |b|
23
- data_store.with_notification_lock(light, Stoplight::Color::RED, Stoplight::Color::GREEN, &b)
24
- end.to yield_control
25
- end
26
- end
27
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base/names'
4
- require_relative 'base/get_failures'
5
- require_relative 'base/get_all'
6
- require_relative 'base/record_failures'
7
- require_relative 'base/clear_failures'
8
- require_relative 'base/get_state'
9
- require_relative 'base/set_state'
10
- require_relative 'base/clear_state'
11
- require_relative 'base/with_notification_lock'
12
-
13
- RSpec.shared_examples 'Stoplight::DataStore::Base' do
14
- it 'is a class' do
15
- expect(described_class).to be_a(Class)
16
- end
17
-
18
- it 'is a subclass of Base' do
19
- expect(described_class).to be < Stoplight::DataStore::Base
20
- end
21
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'redis'
4
- require 'database_cleaner/redis'
5
-
6
- cleaning_strategy = DatabaseCleaner::Redis::Deletion.new(only: ["#{Stoplight::DataStore::Redis::KEY_PREFIX}*"])
7
- DatabaseCleaner.strategy = cleaning_strategy
8
-
9
- RSpec.shared_context :redis, :redis do
10
- let(:redis) { Redis.new(url: ENV.fetch('STOPLIGHT_REDIS_URL', 'redis://127.0.0.1:6379/0')) }
11
-
12
- before(:suite) do
13
- DatabaseCleaner[:redis].db = redis
14
- DatabaseCleaner.clean_with(:deletion)
15
- end
16
-
17
- around(:each) do |example|
18
- DatabaseCleaner.cleaning do
19
- example.run
20
- end
21
- end
22
- end
23
-
24
- RSpec.configure do |config|
25
- config.include_context :redis, include_shared: true
26
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ExceptionHelpers
4
- def suppress(*exception_classes)
5
- yield
6
- rescue Exception => e # rubocop:disable Lint/RescueException
7
- raise unless exception_classes.any? { |cls| e.is_a?(cls) }
8
- end
9
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::Light::Runnable#color' do
4
- subject(:light) { Stoplight::Light.new(name) }
5
-
6
- let(:name) { random_string }
7
-
8
- it 'is initially green' do
9
- expect(light.color).to eql(Stoplight::Color::GREEN)
10
- end
11
-
12
- context 'when its locked green' do
13
- before do
14
- light.data_store.set_state(light, Stoplight::State::LOCKED_GREEN)
15
- end
16
-
17
- it 'is green' do
18
- expect(light.color).to eql(Stoplight::Color::GREEN)
19
- end
20
- end
21
-
22
- context 'when its locked red' do
23
- before do
24
- light.data_store.set_state(light, Stoplight::State::LOCKED_RED)
25
- end
26
-
27
- it 'is red' do
28
- expect(light.color).to eql(Stoplight::Color::RED)
29
- end
30
- end
31
-
32
- context 'when there are many failures' do
33
- let(:anther) { Stoplight::Failure.new(error.class.name, error.message, time - 10) }
34
-
35
- before do
36
- light.with_threshold(2)
37
- light.data_store.record_failure(light, failure)
38
- end
39
-
40
- it 'turns red' do
41
- expect do
42
- light.data_store.record_failure(light, anther)
43
- end.to change(light, :color).to be(Stoplight::Color::RED)
44
- end
45
- end
46
-
47
- context 'when the most recent failure is old' do
48
- let(:failure) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time) }
49
- let(:failure2) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time - 10) }
50
-
51
- before do
52
- light.with_threshold(2)
53
- light.data_store.record_failure(light, failure2)
54
- end
55
-
56
- it 'turns yellow' do
57
- expect do
58
- light.data_store.record_failure(light, failure)
59
- end.to change(light, :color).to be(Stoplight::Color::YELLOW)
60
- end
61
- end
62
-
63
- context 'when the least recent failure is old' do
64
- let(:other) do
65
- Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time)
66
- end
67
-
68
- before do
69
- light.with_threshold(2)
70
- light.data_store.record_failure(light, other)
71
- end
72
-
73
- it 'is red when the least recent failure is old' do
74
- expect do
75
- light.data_store.record_failure(light, failure)
76
- end.to change(light, :color).to be(Stoplight::Color::RED)
77
- end
78
- end
79
- end
@@ -1,247 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::Light::Runnable#run' do
4
- let(:code) { -> { code_result } }
5
- let(:code_result) { random_string }
6
- let(:fallback) { ->(_) { fallback_result } }
7
- let(:fallback_result) { random_string }
8
- let(:name) { random_string }
9
- let(:notifiers) { [notifier] }
10
- let(:notifier) { Stoplight::Notifier::IO.new(io) }
11
- let(:io) { StringIO.new }
12
-
13
- before { light.with_notifiers(notifiers) }
14
-
15
- shared_examples 'when the light is green' do
16
- before { light.data_store.clear_failures(light) }
17
-
18
- it 'runs the code' do
19
- expect(run).to eql(code_result)
20
- end
21
-
22
- context 'with some failures' do
23
- before { light.data_store.record_failure(light, failure) }
24
-
25
- it 'clears the failures' do
26
- run
27
- expect(light.data_store.get_failures(light).size).to eql(0)
28
- end
29
- end
30
-
31
- context 'when the code is failing' do
32
- let(:code_result) { raise error }
33
-
34
- it 're-raises the error' do
35
- expect { run }.to raise_error(error.class)
36
- end
37
-
38
- it 'records the failure' do
39
- expect(light.data_store.get_failures(light).size).to eql(0)
40
- begin
41
- run
42
- rescue error.class
43
- nil
44
- end
45
- expect(light.data_store.get_failures(light).size).to eql(1)
46
- end
47
-
48
- context 'when we did not send notifications yet' do
49
- it 'notifies when transitioning to red' do
50
- light.threshold.times do
51
- expect(io.string).to eql('')
52
- begin
53
- run
54
- rescue error.class
55
- nil
56
- end
57
- end
58
- expect(io.string).to_not eql('')
59
- end
60
- end
61
-
62
- context 'when we already sent notifications' do
63
- before do
64
- light.data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {}
65
- end
66
-
67
- it 'does not send new notifications' do
68
- light.threshold.times do
69
- expect(io.string).to eql('')
70
- begin
71
- run
72
- rescue error.class
73
- nil
74
- end
75
- end
76
- expect(io.string).to eql('')
77
- end
78
- end
79
-
80
- it 'notifies when transitioning to red' do
81
- light.threshold.times do
82
- expect(io.string).to eql('')
83
- begin
84
- run
85
- rescue error.class
86
- nil
87
- end
88
- end
89
- expect(io.string).to_not eql('')
90
- end
91
-
92
- context 'with an error handler' do
93
- let(:result) do
94
- run
95
- expect(false).to be(true)
96
- rescue error.class
97
- expect(true).to be(true)
98
- end
99
-
100
- it 'records the failure when the handler does nothing' do
101
- light.with_error_handler { |_error, _handler| }
102
- expect { result }
103
- .to change { light.data_store.get_failures(light).size }
104
- .by(1)
105
- end
106
-
107
- it 'records the failure when the handler calls handle' do
108
- light.with_error_handler { |error, handle| handle.call(error) }
109
- expect { result }
110
- .to change { light.data_store.get_failures(light).size }
111
- .by(1)
112
- end
113
-
114
- it 'does not record the failure when the handler raises' do
115
- light.with_error_handler { |error, _handle| raise error }
116
- expect { result }
117
- .to_not change { light.data_store.get_failures(light).size }
118
- end
119
- end
120
-
121
- context 'with a fallback' do
122
- before { light.with_fallback(&fallback) }
123
-
124
- it 'runs the fallback' do
125
- expect(run).to eql(fallback_result)
126
- end
127
-
128
- it 'passes the error to the fallback' do
129
- light.with_fallback do |e|
130
- expect(e).to eql(error)
131
- fallback_result
132
- end
133
- expect(run).to eql(fallback_result)
134
- end
135
- end
136
- end
137
-
138
- context 'when the data store is failing' do
139
- let(:error_notifier) { ->(_) {} }
140
- let(:error) { StandardError.new('something went wrong') }
141
-
142
- before do
143
- expect(data_store).to receive(:clear_failures) { raise error }
144
-
145
- light.with_error_notifier(&error_notifier)
146
- end
147
-
148
- it 'runs the code' do
149
- expect(run).to eql(code_result)
150
- end
151
-
152
- it 'notifies about the error' do
153
- has_notified = false
154
- light.with_error_notifier do |e|
155
- has_notified = true
156
- expect(e).to eq(error)
157
- end
158
- run
159
- expect(has_notified).to eql(true)
160
- end
161
- end
162
- end
163
-
164
- shared_examples 'when the light is yellow' do
165
- let(:failure) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time) }
166
- let(:failure2) { Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time - 10) }
167
-
168
- before do
169
- light.with_threshold(2)
170
- light.data_store.record_failure(light, failure2)
171
- light.data_store.record_failure(light, failure)
172
- end
173
-
174
- it 'runs the code' do
175
- expect(run).to eql(code_result)
176
- end
177
-
178
- it 'notifies when transitioning to green' do
179
- expect { run }
180
- .to change(io, :string)
181
- .from(be_empty)
182
- .to(/Switching \w+ from red to green/)
183
- end
184
- end
185
-
186
- shared_examples 'when the light is red' do
187
- let(:other) do
188
- Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time)
189
- end
190
-
191
- before do
192
- light.with_threshold(2)
193
- light.data_store.record_failure(light, other)
194
- light.data_store.record_failure(light, failure)
195
- end
196
-
197
- it 'raises an error' do
198
- expect { run }.to raise_error(Stoplight::Error::RedLight)
199
- end
200
-
201
- it 'uses the name as the error message' do
202
- expect do
203
- run
204
- end.to raise_error(Stoplight::Error::RedLight, light.name)
205
- end
206
-
207
- context 'with a fallback' do
208
- before { light.with_fallback(&fallback) }
209
-
210
- it 'runs the fallback' do
211
- expect(run).to eql(fallback_result)
212
- end
213
-
214
- it 'does not pass anything to the fallback' do
215
- light.with_fallback do |e|
216
- expect(e).to eql(nil)
217
- fallback_result
218
- end
219
- expect(run).to eql(fallback_result)
220
- end
221
- end
222
- end
223
-
224
- context 'with code block' do
225
- subject(:light) { Stoplight::Light.new(name) }
226
-
227
- def run
228
- light.run(&code)
229
- end
230
-
231
- it_behaves_like 'when the light is green'
232
- it_behaves_like 'when the light is yellow'
233
- it_behaves_like 'when the light is red'
234
- end
235
-
236
- context 'without code block' do
237
- subject(:light) { Stoplight::Light.new(name, &code) }
238
-
239
- def run
240
- light.run
241
- end
242
-
243
- it_behaves_like 'when the light is green'
244
- it_behaves_like 'when the light is yellow'
245
- it_behaves_like 'when the light is red'
246
- end
247
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.shared_examples 'Stoplight::Light::Runnable#state' do
4
- subject(:light) { Stoplight::Light.new(name) }
5
-
6
- let(:name) { random_string }
7
-
8
- it 'is initially unlocked' do
9
- expect(light.state).to eql(Stoplight::State::UNLOCKED)
10
- end
11
-
12
- context 'when its locked green' do
13
- before do
14
- light.data_store.set_state(light, Stoplight::State::LOCKED_GREEN)
15
- end
16
-
17
- it 'is locked green' do
18
- expect(light.state).to eql(Stoplight::State::LOCKED_GREEN)
19
- end
20
- end
21
-
22
- context 'when its locked red' do
23
- before do
24
- light.data_store.set_state(light, Stoplight::State::LOCKED_RED)
25
- end
26
-
27
- it 'is locked red' do
28
- expect(light.state).to eql(Stoplight::State::LOCKED_RED)
29
- end
30
- end
31
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'runnable/color'
4
- require_relative 'runnable/run'
5
- require_relative 'runnable/state'