stoplight 5.3.8 → 5.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/views/layout.erb +3 -3
  5. data/lib/stoplight/admin.rb +4 -4
  6. data/lib/stoplight/domain/color.rb +11 -0
  7. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  8. data/lib/stoplight/domain/config.rb +55 -0
  9. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  10. data/lib/stoplight/domain/error.rb +42 -0
  11. data/lib/stoplight/domain/failure.rb +42 -0
  12. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  13. data/lib/stoplight/domain/light.rb +198 -0
  14. data/lib/stoplight/domain/light_factory.rb +75 -0
  15. data/lib/stoplight/domain/metadata.rb +65 -0
  16. data/lib/stoplight/domain/state.rb +11 -0
  17. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  18. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  19. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  20. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  21. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  22. data/lib/stoplight/domain/tracker/base.rb +41 -0
  23. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  24. data/lib/stoplight/domain/tracker/request.rb +67 -0
  25. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  26. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  27. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  28. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  29. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  30. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  31. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  32. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  33. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  34. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  36. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  37. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  38. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  39. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  40. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  41. data/lib/stoplight/version.rb +1 -1
  42. data/lib/stoplight/wiring/container.rb +80 -0
  43. data/lib/stoplight/wiring/default.rb +28 -0
  44. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  45. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  46. data/lib/stoplight/wiring/fail_safe_data_store.rb +123 -0
  47. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  48. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  49. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  50. data/lib/stoplight/wiring/light_factory.rb +188 -0
  51. data/lib/stoplight/wiring/public_api.rb +28 -0
  52. data/lib/stoplight/wiring/system_container.rb +9 -0
  53. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  54. data/lib/stoplight.rb +38 -28
  55. metadata +53 -42
  56. data/lib/stoplight/color.rb +0 -9
  57. data/lib/stoplight/config/dsl.rb +0 -97
  58. data/lib/stoplight/config/library_default_config.rb +0 -21
  59. data/lib/stoplight/config/system_config.rb +0 -7
  60. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  61. data/lib/stoplight/data_store/memory.rb +0 -311
  62. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  63. data/lib/stoplight/data_store/redis.rb +0 -449
  64. data/lib/stoplight/data_store.rb +0 -6
  65. data/lib/stoplight/default.rb +0 -30
  66. data/lib/stoplight/error.rb +0 -10
  67. data/lib/stoplight/failure.rb +0 -71
  68. data/lib/stoplight/light/config.rb +0 -111
  69. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  70. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  71. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  72. data/lib/stoplight/light/run_strategy.rb +0 -32
  73. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  74. data/lib/stoplight/light.rb +0 -191
  75. data/lib/stoplight/metadata.rb +0 -99
  76. data/lib/stoplight/notifier/generic.rb +0 -79
  77. data/lib/stoplight/notifier/io.rb +0 -21
  78. data/lib/stoplight/notifier/logger.rb +0 -19
  79. data/lib/stoplight/state.rb +0 -9
  80. data/lib/stoplight/traffic_control/base.rb +0 -70
  81. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  82. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  83. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  84. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  85. data/lib/stoplight/traffic_recovery.rb +0 -11
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  90. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  91. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Stoplight
6
+ module Infrastructure
7
+ module DataStore
8
+ # @see +Domain::DataStore+
9
+ class Memory < Domain::DataStore
10
+ include MonitorMixin
11
+
12
+ KEY_SEPARATOR = ":"
13
+
14
+ def initialize
15
+ @errors = Hash.new { |errors, light_name| errors[light_name] = SlidingWindow.new }
16
+ @successes = Hash.new { |successes, light_name| successes[light_name] = SlidingWindow.new }
17
+
18
+ @recovery_probe_errors = Hash.new { |recovery_probe_errors, light_name| recovery_probe_errors[light_name] = SlidingWindow.new }
19
+ @recovery_probe_successes = Hash.new { |recovery_probe_successes, light_name| recovery_probe_successes[light_name] = SlidingWindow.new }
20
+
21
+ @metadata = Hash.new do |metadata, light_name|
22
+ metadata[light_name] = Domain::Metadata.new(
23
+ current_time: Time.now,
24
+ successes: 0,
25
+ errors: 0,
26
+ recovery_probe_successes: 0,
27
+ recovery_probe_errors: 0,
28
+ last_error: nil,
29
+ last_error_at: nil,
30
+ last_success_at: nil,
31
+ consecutive_errors: 0,
32
+ consecutive_successes: 0,
33
+ breached_at: nil,
34
+ locked_state: Domain::State::UNLOCKED,
35
+ recovery_scheduled_after: nil,
36
+ recovery_started_at: nil,
37
+ recovered_at: nil
38
+ )
39
+ end
40
+ super # MonitorMixin
41
+ end
42
+
43
+ # @return [Array<String>]
44
+ def names
45
+ synchronize { @metadata.keys }
46
+ end
47
+
48
+ # @param config [Stoplight::Domain::Config]
49
+ # @return [Stoplight::Domain::Metadata]
50
+ def get_metadata(config)
51
+ light_name = config.name
52
+
53
+ synchronize do
54
+ current_time = self.current_time
55
+ recovery_window_start = (current_time - config.cool_off_time)
56
+ recovered_at = @metadata[light_name].recovered_at
57
+ window_start = if config.window_size
58
+ [recovered_at, (current_time - config.window_size)].compact.max
59
+ else
60
+ current_time
61
+ end
62
+
63
+ @metadata[light_name].with(
64
+ current_time:,
65
+ errors: @errors[config.name].sum_in_window(window_start),
66
+ successes: @successes[config.name].sum_in_window(window_start),
67
+ recovery_probe_errors: @recovery_probe_errors[config.name].sum_in_window(recovery_window_start),
68
+ recovery_probe_successes: @recovery_probe_successes[config.name].sum_in_window(recovery_window_start)
69
+ )
70
+ end
71
+ end
72
+
73
+ # @param config [Stoplight::Domain::Config]
74
+ # @param exception [Exception]
75
+ # @return [Stoplight::Domain::Metadata]
76
+ def record_failure(config, exception)
77
+ current_time = self.current_time
78
+ light_name = config.name
79
+ failure = Domain::Failure.from_error(exception, time: current_time)
80
+
81
+ synchronize do
82
+ @errors[light_name].increment if config.window_size
83
+
84
+ metadata = @metadata[light_name]
85
+ @metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
86
+ metadata.with(
87
+ last_error_at: current_time,
88
+ last_error: failure,
89
+ consecutive_errors: metadata.consecutive_errors.succ,
90
+ consecutive_successes: 0
91
+ )
92
+ else
93
+ metadata.with(
94
+ consecutive_errors: metadata.consecutive_errors.succ,
95
+ consecutive_successes: 0
96
+ )
97
+ end
98
+ get_metadata(config)
99
+ end
100
+ end
101
+
102
+ # @param config [Stoplight::Domain::Config]
103
+ # @return [void]
104
+ def record_success(config)
105
+ light_name = config.name
106
+ current_time = self.current_time
107
+
108
+ synchronize do
109
+ @successes[light_name].increment if config.window_size
110
+
111
+ metadata = @metadata[light_name]
112
+ @metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
113
+ metadata.with(
114
+ last_success_at: current_time,
115
+ consecutive_errors: 0,
116
+ consecutive_successes: metadata.consecutive_successes.succ
117
+ )
118
+ else
119
+ metadata.with(
120
+ consecutive_errors: 0,
121
+ consecutive_successes: metadata.consecutive_successes.succ
122
+ )
123
+ end
124
+ end
125
+ end
126
+
127
+ # @param config [Stoplight::Domain::Config]
128
+ # @param exception [Exception]
129
+ # @return [Stoplight::Domain::Metadata]
130
+ def record_recovery_probe_failure(config, exception)
131
+ light_name = config.name
132
+ current_time = self.current_time
133
+ failure = Domain::Failure.from_error(exception, time: current_time)
134
+
135
+ synchronize do
136
+ @recovery_probe_errors[light_name].increment
137
+
138
+ metadata = @metadata[light_name]
139
+ @metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
140
+ metadata.with(
141
+ last_error_at: current_time,
142
+ last_error: failure,
143
+ consecutive_errors: metadata.consecutive_errors.succ,
144
+ consecutive_successes: 0
145
+ )
146
+ else
147
+ metadata.with(
148
+ consecutive_errors: metadata.consecutive_errors.succ,
149
+ consecutive_successes: 0
150
+ )
151
+ end
152
+ get_metadata(config)
153
+ end
154
+ end
155
+
156
+ # @param config [Stoplight::Domain::Config]
157
+ # @return [Stoplight::Domain::Metadata]
158
+ def record_recovery_probe_success(config)
159
+ light_name = config.name
160
+ current_time = self.current_time
161
+
162
+ synchronize do
163
+ @recovery_probe_successes[light_name].increment
164
+
165
+ metadata = @metadata[light_name]
166
+ @metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
167
+ metadata.with(
168
+ last_success_at: current_time,
169
+ consecutive_errors: 0,
170
+ consecutive_successes: metadata.consecutive_successes.succ
171
+ )
172
+ else
173
+ metadata.with(
174
+ consecutive_errors: 0,
175
+ consecutive_successes: metadata.consecutive_successes.succ
176
+ )
177
+ end
178
+ get_metadata(config)
179
+ end
180
+ end
181
+
182
+ # @param config [Stoplight::Domain::Config]
183
+ # @param state [String]
184
+ # @return [String]
185
+ def set_state(config, state)
186
+ light_name = config.name
187
+
188
+ synchronize do
189
+ metadata = @metadata[light_name]
190
+ @metadata[light_name] = metadata.with(locked_state: state)
191
+ end
192
+ state
193
+ end
194
+
195
+ # @return [String]
196
+ def inspect
197
+ "#<#{self.class.name}>"
198
+ end
199
+
200
+ # Combined method that performs the state transition based on color
201
+ #
202
+ # @param config [Stoplight::Domain::Config] The light configuration
203
+ # @param color [String] The color to transition to ("GREEN", "YELLOW", or "RED")
204
+ # @return [Boolean] true if this is the first instance to detect this transition
205
+ def transition_to_color(config, color)
206
+ case color
207
+ when Domain::Color::GREEN
208
+ transition_to_green(config)
209
+ when Domain::Color::YELLOW
210
+ transition_to_yellow(config)
211
+ when Domain::Color::RED
212
+ transition_to_red(config)
213
+ else
214
+ raise ArgumentError, "Invalid color: #{color}"
215
+ end
216
+ end
217
+
218
+ # Transitions to GREEN state and ensures only one notification
219
+ #
220
+ # @param config [Stoplight::Domain::Config] The light configuration
221
+ # @return [Boolean] true if this is the first instance to detect this transition
222
+ private def transition_to_green(config)
223
+ light_name = config.name
224
+ current_time = self.current_time
225
+
226
+ synchronize do
227
+ metadata = @metadata[light_name]
228
+ if metadata.recovered_at
229
+ false
230
+ else
231
+ @metadata[light_name] = metadata.with(
232
+ recovered_at: current_time,
233
+ recovery_started_at: nil,
234
+ breached_at: nil,
235
+ recovery_scheduled_after: nil
236
+ )
237
+ true
238
+ end
239
+ end
240
+ end
241
+
242
+ # Transitions to YELLOW (recovery) state and ensures only one notification
243
+ #
244
+ # @param config [Stoplight::Domain::Config] The light configuration
245
+ # @return [Boolean] true if this is the first instance to detect this transition
246
+ private def transition_to_yellow(config)
247
+ light_name = config.name
248
+ current_time = self.current_time
249
+
250
+ synchronize do
251
+ metadata = @metadata[light_name]
252
+ if metadata.recovery_started_at.nil?
253
+ @metadata[light_name] = metadata.with(
254
+ recovery_started_at: current_time,
255
+ recovery_scheduled_after: nil,
256
+ recovered_at: nil,
257
+ breached_at: nil
258
+ )
259
+ true
260
+ else
261
+ @metadata[light_name] = metadata.with(
262
+ recovery_scheduled_after: nil,
263
+ recovered_at: nil,
264
+ breached_at: nil
265
+ )
266
+ false
267
+ end
268
+ end
269
+ end
270
+
271
+ # Transitions to RED state and ensures only one notification
272
+ #
273
+ # @param config [Stoplight::Domain::Config] The light configuration
274
+ # @return [Boolean] true if this is the first instance to detect this transition
275
+ private def transition_to_red(config)
276
+ light_name = config.name
277
+ current_time = self.current_time
278
+ recovery_scheduled_after = current_time + config.cool_off_time
279
+
280
+ synchronize do
281
+ metadata = @metadata[light_name]
282
+ if metadata.breached_at
283
+ @metadata[light_name] = metadata.with(
284
+ recovery_scheduled_after: recovery_scheduled_after,
285
+ recovery_started_at: nil,
286
+ recovered_at: nil
287
+ )
288
+ false
289
+ else
290
+ @metadata[light_name] = metadata.with(
291
+ breached_at: current_time,
292
+ recovery_scheduled_after: recovery_scheduled_after,
293
+ recovery_started_at: nil,
294
+ recovered_at: nil
295
+ )
296
+ true
297
+ end
298
+ end
299
+ end
300
+
301
+ private def current_time
302
+ Time.now
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Infrastructure
5
+ module DataStore
6
+ class Redis
7
+ # @api private
8
+ module Lua
9
+ class << self
10
+ def read_lua_file(name_without_extension)
11
+ File.read(File.join(__dir__, "#{name_without_extension}.lua"))
12
+ end
13
+ end
14
+
15
+ RECORD_FAILURE = read_lua_file("record_failure")
16
+ RECORD_SUCCESS = read_lua_file("record_success")
17
+ GET_METADATA = read_lua_file("get_metadata")
18
+ TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
19
+ TRANSITION_TO_RED = read_lua_file("transition_to_red")
20
+ TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end