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.
- checksums.yaml +4 -4
- data/README.md +16 -1
- data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
- data/lib/stoplight/admin/views/layout.erb +3 -3
- data/lib/stoplight/admin.rb +4 -4
- data/lib/stoplight/domain/color.rb +11 -0
- data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
- data/lib/stoplight/domain/config.rb +55 -0
- data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
- data/lib/stoplight/domain/error.rb +42 -0
- data/lib/stoplight/domain/failure.rb +42 -0
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
- data/lib/stoplight/domain/light.rb +198 -0
- data/lib/stoplight/domain/light_factory.rb +75 -0
- data/lib/stoplight/domain/metadata.rb +65 -0
- data/lib/stoplight/domain/state.rb +11 -0
- data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
- data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
- data/lib/stoplight/domain/tracker/base.rb +41 -0
- data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
- data/lib/stoplight/domain/tracker/request.rb +67 -0
- data/lib/stoplight/domain/traffic_control/base.rb +74 -0
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
- data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
- data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
- data/lib/stoplight/domain/traffic_recovery.rb +13 -0
- data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
- data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
- data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
- data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
- data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
- data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
- data/lib/stoplight/rspec/generic_notifier.rb +1 -1
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/container.rb +80 -0
- data/lib/stoplight/wiring/default.rb +28 -0
- data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
- data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
- data/lib/stoplight/wiring/fail_safe_data_store.rb +123 -0
- data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
- data/lib/stoplight/wiring/light/default_config.rb +18 -0
- data/lib/stoplight/wiring/light/system_config.rb +11 -0
- data/lib/stoplight/wiring/light_factory.rb +188 -0
- data/lib/stoplight/wiring/public_api.rb +28 -0
- data/lib/stoplight/wiring/system_container.rb +9 -0
- data/lib/stoplight/wiring/system_light_factory.rb +17 -0
- data/lib/stoplight.rb +38 -28
- metadata +53 -42
- data/lib/stoplight/color.rb +0 -9
- data/lib/stoplight/config/dsl.rb +0 -97
- data/lib/stoplight/config/library_default_config.rb +0 -21
- data/lib/stoplight/config/system_config.rb +0 -7
- data/lib/stoplight/data_store/fail_safe.rb +0 -113
- data/lib/stoplight/data_store/memory.rb +0 -311
- data/lib/stoplight/data_store/redis/lua.rb +0 -23
- data/lib/stoplight/data_store/redis.rb +0 -449
- data/lib/stoplight/data_store.rb +0 -6
- data/lib/stoplight/default.rb +0 -30
- data/lib/stoplight/error.rb +0 -10
- data/lib/stoplight/failure.rb +0 -71
- data/lib/stoplight/light/config.rb +0 -111
- data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
- data/lib/stoplight/light/green_run_strategy.rb +0 -54
- data/lib/stoplight/light/red_run_strategy.rb +0 -27
- data/lib/stoplight/light/run_strategy.rb +0 -32
- data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
- data/lib/stoplight/light.rb +0 -191
- data/lib/stoplight/metadata.rb +0 -99
- data/lib/stoplight/notifier/generic.rb +0 -79
- data/lib/stoplight/notifier/io.rb +0 -21
- data/lib/stoplight/notifier/logger.rb +0 -19
- data/lib/stoplight/state.rb +0 -9
- data/lib/stoplight/traffic_control/base.rb +0 -70
- data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
- data/lib/stoplight/traffic_control/error_rate.rb +0 -49
- data/lib/stoplight/traffic_recovery/base.rb +0 -75
- data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
- data/lib/stoplight/traffic_recovery.rb +0 -11
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
- /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_yellow.lua +0 -0
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
module DataStore
|
|
5
|
-
class Redis
|
|
6
|
-
# @api private
|
|
7
|
-
module Lua
|
|
8
|
-
class << self
|
|
9
|
-
def read_lua_file(name_without_extension)
|
|
10
|
-
File.read(File.join(__dir__, "#{name_without_extension}.lua"))
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
RECORD_FAILURE = read_lua_file("record_failure")
|
|
15
|
-
RECORD_SUCCESS = read_lua_file("record_success")
|
|
16
|
-
GET_METADATA = read_lua_file("get_metadata")
|
|
17
|
-
TRANSITION_TO_YELLOW = read_lua_file("transition_to_yellow")
|
|
18
|
-
TRANSITION_TO_RED = read_lua_file("transition_to_red")
|
|
19
|
-
TRANSITION_TO_GREEN = read_lua_file("transition_to_green")
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
|
-
module Stoplight
|
|
6
|
-
module DataStore
|
|
7
|
-
# == Errors
|
|
8
|
-
# All errors are stored in the sorted set where keys are serialized errors and
|
|
9
|
-
# values (Redis uses "score" term) contain integer representations of the time
|
|
10
|
-
# when an error happened.
|
|
11
|
-
#
|
|
12
|
-
# This data structure enables us to query errors that happened within a specific
|
|
13
|
-
# period. We use this feature to support +window_size+ option.
|
|
14
|
-
#
|
|
15
|
-
# To avoid uncontrolled memory consumption, we keep at most +config.threshold+ number
|
|
16
|
-
# of errors happened within last +config.window_size+ seconds (by default infinity).
|
|
17
|
-
#
|
|
18
|
-
# @see Base
|
|
19
|
-
class Redis < Base
|
|
20
|
-
extend Forwardable
|
|
21
|
-
|
|
22
|
-
class << self
|
|
23
|
-
# Generates a Redis key by joining the prefix with the provided pieces.
|
|
24
|
-
#
|
|
25
|
-
# @param pieces [Array<String, Integer>] Parts of the key to be joined.
|
|
26
|
-
# @return [String] The generated Redis key.
|
|
27
|
-
# @api private
|
|
28
|
-
def key(*pieces)
|
|
29
|
-
[KEY_PREFIX, *pieces].join(KEY_SEPARATOR)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Retrieves the list of Redis bucket keys required to cover a specific time window.
|
|
33
|
-
#
|
|
34
|
-
# @param light_name [String] The name of the light (used as part of the Redis key).
|
|
35
|
-
# @param metric [String] The metric type (e.g., "errors").
|
|
36
|
-
# @param window_end [Time, Numeric] The end time of the window (can be a Time object or a numeric timestamp).
|
|
37
|
-
# @param window_size [Numeric] The size of the time window in seconds.
|
|
38
|
-
# @return [Array<String>] A list of Redis keys for the buckets that cover the time window.
|
|
39
|
-
# @api private
|
|
40
|
-
def buckets_for_window(light_name, metric:, window_end:, window_size:)
|
|
41
|
-
window_end_ts = window_end.to_i
|
|
42
|
-
window_start_ts = window_end_ts - [window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
|
43
|
-
|
|
44
|
-
# Find bucket timestamps that contain any part of the window
|
|
45
|
-
start_bucket = (window_start_ts / bucket_size) * bucket_size
|
|
46
|
-
|
|
47
|
-
# End bucket is the last bucket that contains data within our window
|
|
48
|
-
end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size
|
|
49
|
-
|
|
50
|
-
(start_bucket..end_bucket).step(bucket_size).map do |bucket_start|
|
|
51
|
-
bucket_key(light_name, metric: metric, time: bucket_start)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Generates a Redis key for a specific metric and time.
|
|
56
|
-
#
|
|
57
|
-
# @param light_name [String] The name of the light.
|
|
58
|
-
# @param metric [String] The metric type (e.g., "errors").
|
|
59
|
-
# @param time [Time, Numeric] The time for which to generate the key.
|
|
60
|
-
# @return [String] The generated Redis key.
|
|
61
|
-
def bucket_key(light_name, metric:, time:)
|
|
62
|
-
key("metrics", light_name, metric, (time.to_i / bucket_size) * bucket_size)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
BUCKET_SIZE = 3600 # 1h
|
|
66
|
-
private_constant :BUCKET_SIZE
|
|
67
|
-
|
|
68
|
-
private def bucket_size
|
|
69
|
-
BUCKET_SIZE
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
KEY_SEPARATOR = ":"
|
|
74
|
-
KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
|
|
75
|
-
|
|
76
|
-
# @param redis [::Redis, ConnectionPool<::Redis>]
|
|
77
|
-
# @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
|
|
78
|
-
# the application server
|
|
79
|
-
def initialize(redis, warn_on_clock_skew: true)
|
|
80
|
-
@warn_on_clock_skew = warn_on_clock_skew
|
|
81
|
-
@redis = redis
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def names
|
|
85
|
-
pattern = key("metadata", "*")
|
|
86
|
-
prefix_regex = /^#{key("metadata", "")}/
|
|
87
|
-
@redis.then do |client|
|
|
88
|
-
client.scan_each(match: pattern).to_a.map do |key|
|
|
89
|
-
key.sub(prefix_regex, "")
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def get_metadata(config)
|
|
95
|
-
detect_clock_skew
|
|
96
|
-
|
|
97
|
-
current_time = Time.now
|
|
98
|
-
window_end_ts = current_time.to_i
|
|
99
|
-
window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
|
100
|
-
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
|
101
|
-
|
|
102
|
-
if config.window_size
|
|
103
|
-
failure_keys = failure_bucket_keys(config, window_end: window_end_ts)
|
|
104
|
-
success_keys = success_bucket_keys(config, window_end: window_end_ts)
|
|
105
|
-
else
|
|
106
|
-
failure_keys = []
|
|
107
|
-
success_keys = []
|
|
108
|
-
end
|
|
109
|
-
recovery_probe_failure_keys = recovery_probe_failure_bucket_keys(config, window_end: window_end_ts)
|
|
110
|
-
recovery_probe_success_keys = recovery_probe_success_bucket_keys(config, window_end: window_end_ts)
|
|
111
|
-
|
|
112
|
-
successes, errors, recovery_probe_successes, recovery_probe_errors, meta = @redis.with do |client|
|
|
113
|
-
client.evalsha(
|
|
114
|
-
get_metadata_sha,
|
|
115
|
-
argv: [
|
|
116
|
-
failure_keys.count,
|
|
117
|
-
recovery_probe_failure_keys.count,
|
|
118
|
-
window_start_ts,
|
|
119
|
-
window_end_ts,
|
|
120
|
-
recovery_window_start_ts
|
|
121
|
-
],
|
|
122
|
-
keys: [
|
|
123
|
-
metadata_key(config),
|
|
124
|
-
*success_keys,
|
|
125
|
-
*failure_keys,
|
|
126
|
-
*recovery_probe_success_keys,
|
|
127
|
-
*recovery_probe_failure_keys
|
|
128
|
-
]
|
|
129
|
-
)
|
|
130
|
-
end
|
|
131
|
-
meta_hash = meta.each_slice(2).to_h.transform_keys(&:to_sym)
|
|
132
|
-
last_error_json = meta_hash.delete(:last_error_json)
|
|
133
|
-
last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
|
|
134
|
-
|
|
135
|
-
Metadata.new(
|
|
136
|
-
current_time:,
|
|
137
|
-
successes:,
|
|
138
|
-
errors:,
|
|
139
|
-
recovery_probe_successes:,
|
|
140
|
-
recovery_probe_errors:,
|
|
141
|
-
last_error:,
|
|
142
|
-
**meta_hash
|
|
143
|
-
)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# @param config [Stoplight::Light::Config] The light configuration.
|
|
147
|
-
# @param failure [Stoplight::Failure] The failure to record.
|
|
148
|
-
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
|
149
|
-
def record_failure(config, failure)
|
|
150
|
-
current_ts = failure.time.to_i
|
|
151
|
-
failure_json = failure.to_json
|
|
152
|
-
|
|
153
|
-
@redis.then do |client|
|
|
154
|
-
client.evalsha(
|
|
155
|
-
record_failure_sha,
|
|
156
|
-
argv: [current_ts, SecureRandom.hex(12), failure_json, metrics_ttl, metadata_ttl],
|
|
157
|
-
keys: [
|
|
158
|
-
metadata_key(config),
|
|
159
|
-
config.window_size && errors_key(config, time: current_ts)
|
|
160
|
-
].compact
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
get_metadata(config)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def record_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
|
167
|
-
request_ts = request_time.to_i
|
|
168
|
-
|
|
169
|
-
@redis.then do |client|
|
|
170
|
-
client.evalsha(
|
|
171
|
-
record_success_sha,
|
|
172
|
-
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
|
173
|
-
keys: [
|
|
174
|
-
metadata_key(config),
|
|
175
|
-
config.window_size && successes_key(config, time: request_ts)
|
|
176
|
-
].compact
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Records a failed recovery probe for a specific light configuration.
|
|
182
|
-
#
|
|
183
|
-
# @param config [Stoplight::Light::Config] The light configuration.
|
|
184
|
-
# @param failure [Failure] The failure to record.
|
|
185
|
-
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
|
186
|
-
def record_recovery_probe_failure(config, failure)
|
|
187
|
-
current_ts = failure.time.to_i
|
|
188
|
-
failure_json = failure.to_json
|
|
189
|
-
|
|
190
|
-
@redis.then do |client|
|
|
191
|
-
client.evalsha(
|
|
192
|
-
record_failure_sha,
|
|
193
|
-
argv: [current_ts, SecureRandom.uuid, failure_json, metrics_ttl, metrics_ttl],
|
|
194
|
-
keys: [
|
|
195
|
-
metadata_key(config),
|
|
196
|
-
recovery_probe_errors_key(config, time: current_ts)
|
|
197
|
-
].compact
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
get_metadata(config)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Records a successful recovery probe for a specific light configuration.
|
|
204
|
-
#
|
|
205
|
-
# @param config [Stoplight::Light::Config] The light configuration.
|
|
206
|
-
# @param request_id [String] The unique identifier for the request
|
|
207
|
-
# @param request_time [Time] The time of the request
|
|
208
|
-
# @return [Stoplight::Metadata] The updated metadata after recording the success.
|
|
209
|
-
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12), request_time: Time.now)
|
|
210
|
-
request_ts = request_time.to_i
|
|
211
|
-
|
|
212
|
-
@redis.then do |client|
|
|
213
|
-
client.evalsha(
|
|
214
|
-
record_success_sha,
|
|
215
|
-
argv: [request_ts, request_id, metrics_ttl, metadata_ttl],
|
|
216
|
-
keys: [
|
|
217
|
-
metadata_key(config),
|
|
218
|
-
recovery_probe_successes_key(config, time: request_ts)
|
|
219
|
-
].compact
|
|
220
|
-
)
|
|
221
|
-
end
|
|
222
|
-
get_metadata(config)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def set_state(config, state)
|
|
226
|
-
@redis.then do |client|
|
|
227
|
-
client.hset(metadata_key(config), "locked_state", state)
|
|
228
|
-
end
|
|
229
|
-
state
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def inspect
|
|
233
|
-
"#<#{self.class.name} redis=#{@redis.inspect}>"
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Combined method that performs the state transition based on color
|
|
237
|
-
#
|
|
238
|
-
# @param config [Stoplight::Light::Config] The light configuration
|
|
239
|
-
# @param color [String] The color to transition to ("green", "yellow", or "red")
|
|
240
|
-
# @param current_time [Time] Current timestamp
|
|
241
|
-
# @return [Boolean] true if this is the first instance to detect this transition
|
|
242
|
-
def transition_to_color(config, color, current_time: Time.now)
|
|
243
|
-
current_time.to_i
|
|
244
|
-
|
|
245
|
-
case color
|
|
246
|
-
when Color::GREEN
|
|
247
|
-
transition_to_green(config)
|
|
248
|
-
when Color::YELLOW
|
|
249
|
-
transition_to_yellow(config, current_time:)
|
|
250
|
-
when Color::RED
|
|
251
|
-
transition_to_red(config, current_time:)
|
|
252
|
-
else
|
|
253
|
-
raise ArgumentError, "Invalid color: #{color}"
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Transitions to GREEN state and ensures only one notification
|
|
258
|
-
#
|
|
259
|
-
# @param config [Stoplight::Light::Config] The light configuration
|
|
260
|
-
# @return [Boolean] true if this is the first instance to detect this transition
|
|
261
|
-
private def transition_to_green(config, current_time: Time.now)
|
|
262
|
-
current_ts = current_time.to_i
|
|
263
|
-
meta_key = metadata_key(config)
|
|
264
|
-
|
|
265
|
-
became_green = @redis.then do |client|
|
|
266
|
-
client.evalsha(
|
|
267
|
-
transition_to_green_sha,
|
|
268
|
-
argv: [current_ts],
|
|
269
|
-
keys: [meta_key]
|
|
270
|
-
)
|
|
271
|
-
end
|
|
272
|
-
became_green == 1
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
276
|
-
#
|
|
277
|
-
# @param config [Stoplight::Light::Config] The light configuration
|
|
278
|
-
# @param current_time [Time] Current timestamp
|
|
279
|
-
# @return [Boolean] true if this is the first instance to detect this transition
|
|
280
|
-
private def transition_to_yellow(config, current_time: Time.now)
|
|
281
|
-
current_ts = current_time.to_i
|
|
282
|
-
meta_key = metadata_key(config)
|
|
283
|
-
|
|
284
|
-
became_yellow = @redis.then do |client|
|
|
285
|
-
client.evalsha(
|
|
286
|
-
transition_to_yellow_sha,
|
|
287
|
-
argv: [current_ts],
|
|
288
|
-
keys: [meta_key]
|
|
289
|
-
)
|
|
290
|
-
end
|
|
291
|
-
became_yellow == 1
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Transitions to RED state and ensures only one notification
|
|
295
|
-
#
|
|
296
|
-
# @param config [Stoplight::Light::Config] The light configuration
|
|
297
|
-
# @param current_time [Time] Current timestamp
|
|
298
|
-
# @return [Boolean] true if this is the first instance to detect this transition
|
|
299
|
-
private def transition_to_red(config, current_time: Time.now)
|
|
300
|
-
current_ts = current_time.to_i
|
|
301
|
-
meta_key = metadata_key(config)
|
|
302
|
-
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
303
|
-
|
|
304
|
-
became_red = @redis.then do |client|
|
|
305
|
-
client.evalsha(
|
|
306
|
-
transition_to_red_sha,
|
|
307
|
-
argv: [current_ts, recovery_scheduled_after_ts],
|
|
308
|
-
keys: [meta_key]
|
|
309
|
-
)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
became_red == 1
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
private def normalize_failure(failure, error_notifier)
|
|
316
|
-
Failure.from_json(failure)
|
|
317
|
-
rescue => e
|
|
318
|
-
error_notifier.call(e)
|
|
319
|
-
Failure.from_error(e)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def_delegator "self.class", :key
|
|
323
|
-
|
|
324
|
-
private def failure_bucket_keys(config, window_end:)
|
|
325
|
-
self.class.buckets_for_window(
|
|
326
|
-
config.name,
|
|
327
|
-
metric: "failure",
|
|
328
|
-
window_end: window_end,
|
|
329
|
-
window_size: config.window_size
|
|
330
|
-
)
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
private def success_bucket_keys(config, window_end:)
|
|
334
|
-
self.class.buckets_for_window(
|
|
335
|
-
config.name,
|
|
336
|
-
metric: "success",
|
|
337
|
-
window_end: window_end,
|
|
338
|
-
window_size: config.window_size
|
|
339
|
-
)
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
private def recovery_probe_failure_bucket_keys(config, window_end:)
|
|
343
|
-
self.class.buckets_for_window(
|
|
344
|
-
config.name,
|
|
345
|
-
metric: "recovery_probe_failure",
|
|
346
|
-
window_end: window_end,
|
|
347
|
-
window_size: config.cool_off_time
|
|
348
|
-
)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
private def recovery_probe_success_bucket_keys(config, window_end:)
|
|
352
|
-
self.class.buckets_for_window(
|
|
353
|
-
config.name,
|
|
354
|
-
metric: "recovery_probe_success",
|
|
355
|
-
window_end: window_end,
|
|
356
|
-
window_size: config.cool_off_time
|
|
357
|
-
)
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
private def successes_key(config, time:)
|
|
361
|
-
self.class.bucket_key(config.name, metric: "success", time:)
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
private def errors_key(config, time:)
|
|
365
|
-
self.class.bucket_key(config.name, metric: "failure", time:)
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
private def recovery_probe_successes_key(config, time:)
|
|
369
|
-
self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
private def recovery_probe_errors_key(config, time:)
|
|
373
|
-
self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
private def metadata_key(config)
|
|
377
|
-
key("metadata", config.name)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
METRICS_TTL = 86400 # 1 day
|
|
381
|
-
private_constant :METRICS_TTL
|
|
382
|
-
|
|
383
|
-
private def metrics_ttl
|
|
384
|
-
METRICS_TTL
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
METADATA_TTL = 86400 * 7 # 7 days
|
|
388
|
-
private_constant :METADATA_TTL
|
|
389
|
-
|
|
390
|
-
private def metadata_ttl
|
|
391
|
-
METADATA_TTL
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
SKEW_TOLERANCE = 5 # seconds
|
|
395
|
-
private_constant :SKEW_TOLERANCE
|
|
396
|
-
|
|
397
|
-
private def detect_clock_skew
|
|
398
|
-
return unless @warn_on_clock_skew
|
|
399
|
-
return unless should_sample?(0.01) # 1% chance
|
|
400
|
-
|
|
401
|
-
redis_seconds, _redis_millis = @redis.then(&:time)
|
|
402
|
-
app_seconds = Time.now.to_i
|
|
403
|
-
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
|
404
|
-
warn("Detected clock skew between Redis and the application server. Redis time: #{redis_seconds}, Application time: #{app_seconds}. See https://github.com/bolshakov/stoplight/wiki/Clock-Skew-and-Stoplight-Reliability")
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
private def should_sample?(probability)
|
|
409
|
-
rand <= probability
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
private def record_success_sha
|
|
413
|
-
@record_success_sha ||= @redis.then do |client|
|
|
414
|
-
client.script("load", Lua::RECORD_SUCCESS)
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
private def get_metadata_sha
|
|
419
|
-
@get_metadata_sha ||= @redis.then do |client|
|
|
420
|
-
client.script("load", Lua::GET_METADATA)
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
private def transition_to_yellow_sha
|
|
425
|
-
@transition_to_yellow_sha ||= @redis.then do |client|
|
|
426
|
-
client.script("load", Lua::TRANSITION_TO_YELLOW)
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
private def transition_to_red_sha
|
|
431
|
-
@transition_to_red_sha ||= @redis.then do |client|
|
|
432
|
-
client.script("load", Lua::TRANSITION_TO_RED)
|
|
433
|
-
end
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
private def transition_to_green_sha
|
|
437
|
-
@transition_to_green_sha ||= @redis.then do |client|
|
|
438
|
-
client.script("load", Lua::TRANSITION_TO_GREEN)
|
|
439
|
-
end
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
private def record_failure_sha
|
|
443
|
-
@record_failure_sha ||= @redis.then do |client|
|
|
444
|
-
client.script("load", Lua::RECORD_FAILURE)
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
end
|
data/lib/stoplight/data_store.rb
DELETED
data/lib/stoplight/default.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
module Default
|
|
5
|
-
COOL_OFF_TIME = 60.0
|
|
6
|
-
|
|
7
|
-
DATA_STORE = DataStore::Memory.new
|
|
8
|
-
|
|
9
|
-
ERROR_NOTIFIER = ->(error) { warn error }
|
|
10
|
-
|
|
11
|
-
FORMATTER = lambda do |light, from_color, to_color, error|
|
|
12
|
-
words = ["Switching", light.name, "from", from_color, "to", to_color]
|
|
13
|
-
words += ["because", error.class, error.message] if error
|
|
14
|
-
words.join(" ")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
NOTIFIERS = [Notifier::IO.new($stderr)].freeze
|
|
18
|
-
|
|
19
|
-
THRESHOLD = 3
|
|
20
|
-
RECOVERY_THRESHOLD = 1
|
|
21
|
-
|
|
22
|
-
WINDOW_SIZE = nil
|
|
23
|
-
|
|
24
|
-
TRACKED_ERRORS = [StandardError].freeze
|
|
25
|
-
SKIPPED_ERRORS = [].freeze
|
|
26
|
-
|
|
27
|
-
TRAFFIC_CONTROL = TrafficControl::ConsecutiveErrors.new
|
|
28
|
-
TRAFFIC_RECOVERY = TrafficRecovery::ConsecutiveSuccesses.new
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/stoplight/error.rb
DELETED
data/lib/stoplight/failure.rb
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "time"
|
|
5
|
-
|
|
6
|
-
module Stoplight
|
|
7
|
-
class Failure # rubocop:disable Style/Documentation
|
|
8
|
-
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%N%:z"
|
|
9
|
-
|
|
10
|
-
# @return [String]
|
|
11
|
-
attr_reader :error_class
|
|
12
|
-
# @return [String]
|
|
13
|
-
attr_reader :error_message
|
|
14
|
-
# @return [Time]
|
|
15
|
-
attr_reader :time
|
|
16
|
-
|
|
17
|
-
# @param error [Exception]
|
|
18
|
-
# @return (see #initialize)
|
|
19
|
-
def self.from_error(error, time: Time.now)
|
|
20
|
-
new(error.class.name, error.message, time)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# @param json [String]
|
|
24
|
-
# @return (see #initialize)
|
|
25
|
-
# @raise [JSON::ParserError]
|
|
26
|
-
# @raise [ArgumentError]
|
|
27
|
-
def self.from_json(json)
|
|
28
|
-
object = JSON.parse(json)
|
|
29
|
-
error_object = object["error"]
|
|
30
|
-
|
|
31
|
-
error_class = error_object["class"]
|
|
32
|
-
error_message = error_object["message"]
|
|
33
|
-
time = Time.at(object["time"])
|
|
34
|
-
|
|
35
|
-
new(error_class, error_message, time)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# @param error_class [String]
|
|
39
|
-
# @param error_message [String]
|
|
40
|
-
# @param time [Time]
|
|
41
|
-
def initialize(error_class, error_message, time)
|
|
42
|
-
@error_class = error_class
|
|
43
|
-
@error_message = error_message
|
|
44
|
-
@time = Time.at(time.to_i) # truncate to seconds
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# @param other [Failure]
|
|
48
|
-
# @return [Boolean]
|
|
49
|
-
def ==(other)
|
|
50
|
-
other.is_a?(self.class) &&
|
|
51
|
-
error_class == other.error_class &&
|
|
52
|
-
error_message == other.error_message &&
|
|
53
|
-
time.to_i == other.time.to_i
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# @param options [Object, nil]
|
|
57
|
-
# @return [String]
|
|
58
|
-
def to_json(options = nil)
|
|
59
|
-
JSON.generate(
|
|
60
|
-
{
|
|
61
|
-
error: {
|
|
62
|
-
class: error_class,
|
|
63
|
-
message: error_message
|
|
64
|
-
},
|
|
65
|
-
time: time.to_i
|
|
66
|
-
},
|
|
67
|
-
options
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
class Light
|
|
5
|
-
# A +Stoplight::Light+ configuration object.
|
|
6
|
-
#
|
|
7
|
-
# # @!attribute [r] name
|
|
8
|
-
# @return [String]
|
|
9
|
-
#
|
|
10
|
-
# @!attribute [r] cool_off_time
|
|
11
|
-
# @return [Numeric]
|
|
12
|
-
#
|
|
13
|
-
# @!attribute [r] data_store
|
|
14
|
-
# @return [Stoplight::DataStore::Base]
|
|
15
|
-
#
|
|
16
|
-
# @!attribute [r] error_notifier
|
|
17
|
-
# @return [StandardError => void]
|
|
18
|
-
#
|
|
19
|
-
# @!attribute [r] notifiers
|
|
20
|
-
# @return [Array<Stoplight::Notifier::Base>]
|
|
21
|
-
#
|
|
22
|
-
# @!attribute [r] threshold
|
|
23
|
-
# @return [Numeric]
|
|
24
|
-
#
|
|
25
|
-
# @!attribute [r] window_size
|
|
26
|
-
# @return [Numeric]
|
|
27
|
-
#
|
|
28
|
-
# @!attribute [r] tracked_errors
|
|
29
|
-
# @return [Array<StandardError>]
|
|
30
|
-
#
|
|
31
|
-
# @!attribute [r] skipped_errors
|
|
32
|
-
# @return [Array<Exception>]
|
|
33
|
-
#
|
|
34
|
-
# @!attribute [r] traffic_control
|
|
35
|
-
# @return [Stoplight::TrafficControl::Base]
|
|
36
|
-
#
|
|
37
|
-
# @!attribute [r] traffic_recovery
|
|
38
|
-
# @return [Stoplight::TrafficRecovery::Base]
|
|
39
|
-
# @api private
|
|
40
|
-
Config = Data.define(
|
|
41
|
-
:name,
|
|
42
|
-
:cool_off_time,
|
|
43
|
-
:data_store,
|
|
44
|
-
:error_notifier,
|
|
45
|
-
:notifiers,
|
|
46
|
-
:threshold,
|
|
47
|
-
:recovery_threshold,
|
|
48
|
-
:window_size,
|
|
49
|
-
:tracked_errors,
|
|
50
|
-
:skipped_errors,
|
|
51
|
-
:traffic_control,
|
|
52
|
-
:traffic_recovery
|
|
53
|
-
) do
|
|
54
|
-
class << self
|
|
55
|
-
# Creates a new NULL configuration object.
|
|
56
|
-
# @return [Stoplight::Light::Config]
|
|
57
|
-
def empty
|
|
58
|
-
new(**members.map { |key| [key, nil] }.to_h)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Checks if the given error should be tracked
|
|
63
|
-
#
|
|
64
|
-
# @param error [#==] The error to check, e.g. an Exception, Class or Proc
|
|
65
|
-
# @return [Boolean]
|
|
66
|
-
def track_error?(error)
|
|
67
|
-
skip = skipped_errors.any? { |klass| klass === error }
|
|
68
|
-
track = tracked_errors.any? { |klass| klass === error }
|
|
69
|
-
|
|
70
|
-
!skip && track
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# This method applies configuration dsl and revalidates the configuration
|
|
74
|
-
# @return [Stoplight::Light::Config]
|
|
75
|
-
def with(**settings)
|
|
76
|
-
super(**CONFIG_DSL.transform(settings)).then do |config|
|
|
77
|
-
config.validate_config!
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# @raise [Stoplight::Error::ConfigurationError]
|
|
82
|
-
# @return [Stoplight::Light::Config] The validated configuration object.
|
|
83
|
-
def validate_config!
|
|
84
|
-
validate_traffic_control_compatibility!
|
|
85
|
-
self
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
def validate_traffic_control_compatibility!
|
|
91
|
-
traffic_control.check_compatibility(self).then do |compatibility_result|
|
|
92
|
-
if compatibility_result.incompatible?
|
|
93
|
-
raise Stoplight::Error::ConfigurationError.new(
|
|
94
|
-
"#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def validate_traffic_recovery_compatibility!
|
|
101
|
-
traffic_recovery.check_compatibility(self).then do |compatibility_result|
|
|
102
|
-
if compatibility_result.incompatible?
|
|
103
|
-
raise Stoplight::Error::ConfigurationError.new(
|
|
104
|
-
"#{traffic_control.class.name} strategy is incompatible with the Stoplight configuration: #{compatibility_result.error_messages}"
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|