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
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Infrastructure
|
|
7
|
+
module DataStore
|
|
8
|
+
# == Errors
|
|
9
|
+
# All errors are stored in the sorted set where keys are serialized errors and
|
|
10
|
+
# values (Redis uses "score" term) contain integer representations of the time
|
|
11
|
+
# when an error happened.
|
|
12
|
+
#
|
|
13
|
+
# This data structure enables us to query errors that happened within a specific
|
|
14
|
+
# period. We use this feature to support +window_size+ option.
|
|
15
|
+
#
|
|
16
|
+
# To avoid uncontrolled memory consumption, we keep at most +config.threshold+ number
|
|
17
|
+
# of errors happened within last +config.window_size+ seconds (by default infinity).
|
|
18
|
+
#
|
|
19
|
+
# @see Base
|
|
20
|
+
class Redis < Domain::DataStore
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Generates a Redis key by joining the prefix with the provided pieces.
|
|
25
|
+
#
|
|
26
|
+
# @param pieces [Array<String, Integer>] Parts of the key to be joined.
|
|
27
|
+
# @return [String] The generated Redis key.
|
|
28
|
+
# @api private
|
|
29
|
+
def key(*pieces)
|
|
30
|
+
[KEY_PREFIX, *pieces].join(KEY_SEPARATOR)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Retrieves the list of Redis bucket keys required to cover a specific time window.
|
|
34
|
+
#
|
|
35
|
+
# @param light_name [String] The name of the light (used as part of the Redis key).
|
|
36
|
+
# @param metric [String] The metric type (e.g., "errors").
|
|
37
|
+
# @param window_end [Time, Numeric] The end time of the window (can be a Time object or a numeric timestamp).
|
|
38
|
+
# @param window_size [Numeric] The size of the time window in seconds.
|
|
39
|
+
# @return [Array<String>] A list of Redis keys for the buckets that cover the time window.
|
|
40
|
+
# @api private
|
|
41
|
+
def buckets_for_window(light_name, metric:, window_end:, window_size:)
|
|
42
|
+
window_end_ts = window_end.to_i
|
|
43
|
+
window_start_ts = window_end_ts - [window_size, Domain::DataStore::METRICS_RETENTION_TIME].compact.min.to_i
|
|
44
|
+
|
|
45
|
+
# Find bucket timestamps that contain any part of the window
|
|
46
|
+
start_bucket = (window_start_ts / bucket_size) * bucket_size
|
|
47
|
+
|
|
48
|
+
# End bucket is the last bucket that contains data within our window
|
|
49
|
+
end_bucket = ((window_end_ts - 1) / bucket_size) * bucket_size
|
|
50
|
+
|
|
51
|
+
(start_bucket..end_bucket).step(bucket_size).map do |bucket_start|
|
|
52
|
+
bucket_key(light_name, metric: metric, time: bucket_start)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Generates a Redis key for a specific metric and time.
|
|
57
|
+
#
|
|
58
|
+
# @param light_name [String] The name of the light.
|
|
59
|
+
# @param metric [String] The metric type (e.g., "errors").
|
|
60
|
+
# @param time [Time, Numeric] The time for which to generate the key.
|
|
61
|
+
# @return [String] The generated Redis key.
|
|
62
|
+
def bucket_key(light_name, metric:, time:)
|
|
63
|
+
key("metrics", light_name, metric, (time.to_i / bucket_size) * bucket_size)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
BUCKET_SIZE = 3600 # 1h
|
|
67
|
+
private_constant :BUCKET_SIZE
|
|
68
|
+
|
|
69
|
+
private def bucket_size
|
|
70
|
+
BUCKET_SIZE
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
KEY_SEPARATOR = ":"
|
|
75
|
+
KEY_PREFIX = %w[stoplight v5].join(KEY_SEPARATOR)
|
|
76
|
+
|
|
77
|
+
# @param redis [::Redis, ConnectionPool<::Redis>]
|
|
78
|
+
# @param warn_on_clock_skew [Boolean] (true) Whether to warn about clock skew between Redis and
|
|
79
|
+
# the application server
|
|
80
|
+
def initialize(redis, warn_on_clock_skew: true)
|
|
81
|
+
@warn_on_clock_skew = warn_on_clock_skew
|
|
82
|
+
@redis = redis
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def names
|
|
86
|
+
pattern = key("metadata", "*")
|
|
87
|
+
prefix_regex = /^#{key("metadata", "")}/
|
|
88
|
+
@redis.then do |client|
|
|
89
|
+
client.scan_each(match: pattern).to_a.map do |key|
|
|
90
|
+
key.sub(prefix_regex, "")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def get_metadata(config)
|
|
96
|
+
detect_clock_skew
|
|
97
|
+
|
|
98
|
+
window_end_ts = current_time.to_f
|
|
99
|
+
window_start_ts = window_end_ts - [config.window_size, METRICS_RETENTION_TIME].compact.min.to_f
|
|
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 = deserialize_failure(last_error_json) if last_error_json
|
|
134
|
+
|
|
135
|
+
Domain::Metadata.new(
|
|
136
|
+
current_time:,
|
|
137
|
+
successes:,
|
|
138
|
+
errors:,
|
|
139
|
+
recovery_probe_successes:,
|
|
140
|
+
recovery_probe_errors:,
|
|
141
|
+
last_error:,
|
|
142
|
+
last_error_at: (Time.at(meta_hash[:last_error_at].to_f) if meta_hash[:last_error_at]),
|
|
143
|
+
last_success_at: (Time.at(meta_hash[:last_success_at].to_f) if meta_hash[:last_success_at]),
|
|
144
|
+
consecutive_errors: meta_hash[:consecutive_errors].to_i,
|
|
145
|
+
consecutive_successes: meta_hash[:consecutive_successes].to_i,
|
|
146
|
+
breached_at: (Time.at(meta_hash[:breached_at].to_f) if meta_hash[:breached_at]),
|
|
147
|
+
locked_state: meta_hash[:locked_state] || Domain::State::UNLOCKED,
|
|
148
|
+
recovery_scheduled_after: (Time.at(meta_hash[:recovery_scheduled_after].to_f) if meta_hash[:recovery_scheduled_after]),
|
|
149
|
+
recovery_started_at: (Time.at(meta_hash[:recovery_started_at].to_f) if meta_hash[:recovery_started_at]),
|
|
150
|
+
recovered_at: (Time.at(meta_hash[:recovered_at].to_f) if meta_hash[:recovered_at])
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
155
|
+
# @param exception [Exception]
|
|
156
|
+
# @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
|
|
157
|
+
def record_failure(config, exception)
|
|
158
|
+
current_time = self.current_time
|
|
159
|
+
current_ts = current_time.to_f
|
|
160
|
+
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
161
|
+
|
|
162
|
+
@redis.then do |client|
|
|
163
|
+
client.evalsha(
|
|
164
|
+
record_failure_sha,
|
|
165
|
+
argv: [current_ts, SecureRandom.hex(12), serialize_failure(failure), metrics_ttl, metadata_ttl],
|
|
166
|
+
keys: [
|
|
167
|
+
metadata_key(config),
|
|
168
|
+
config.window_size && errors_key(config, time: current_ts)
|
|
169
|
+
].compact
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
get_metadata(config)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def record_success(config, request_id: SecureRandom.hex(12))
|
|
176
|
+
current_ts = current_time.to_f
|
|
177
|
+
|
|
178
|
+
@redis.then do |client|
|
|
179
|
+
client.evalsha(
|
|
180
|
+
record_success_sha,
|
|
181
|
+
argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
|
|
182
|
+
keys: [
|
|
183
|
+
metadata_key(config),
|
|
184
|
+
config.window_size && successes_key(config, time: current_ts)
|
|
185
|
+
].compact
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Records a failed recovery probe for a specific light configuration.
|
|
191
|
+
#
|
|
192
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
193
|
+
# @param exception [Exception]
|
|
194
|
+
# @return [Stoplight::Domain::Metadata] The updated metadata after recording the failure.
|
|
195
|
+
def record_recovery_probe_failure(config, exception)
|
|
196
|
+
current_time = self.current_time
|
|
197
|
+
current_ts = current_time.to_f
|
|
198
|
+
failure = Domain::Failure.from_error(exception, time: current_time)
|
|
199
|
+
|
|
200
|
+
@redis.then do |client|
|
|
201
|
+
client.evalsha(
|
|
202
|
+
record_failure_sha,
|
|
203
|
+
argv: [current_ts, SecureRandom.uuid, serialize_failure(failure), metrics_ttl, metrics_ttl],
|
|
204
|
+
keys: [
|
|
205
|
+
metadata_key(config),
|
|
206
|
+
recovery_probe_errors_key(config, time: current_ts)
|
|
207
|
+
].compact
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
get_metadata(config)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Records a successful recovery probe for a specific light configuration.
|
|
214
|
+
#
|
|
215
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
216
|
+
# @param request_id [String] The unique identifier for the request
|
|
217
|
+
# @return [Stoplight::Domain::Metadata] The updated metadata after recording the success.
|
|
218
|
+
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
|
|
219
|
+
current_ts = current_time.to_f
|
|
220
|
+
|
|
221
|
+
@redis.then do |client|
|
|
222
|
+
client.evalsha(
|
|
223
|
+
record_success_sha,
|
|
224
|
+
argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
|
|
225
|
+
keys: [
|
|
226
|
+
metadata_key(config),
|
|
227
|
+
recovery_probe_successes_key(config, time: current_ts)
|
|
228
|
+
].compact
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
get_metadata(config)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def set_state(config, state)
|
|
235
|
+
@redis.then do |client|
|
|
236
|
+
client.hset(metadata_key(config), "locked_state", state)
|
|
237
|
+
end
|
|
238
|
+
state
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def inspect
|
|
242
|
+
"#<#{self.class.name} redis=#{@redis.inspect}>"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Combined method that performs the state transition based on color
|
|
246
|
+
#
|
|
247
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
248
|
+
# @param color [String] The color to transition to ("green", "yellow", or "red")
|
|
249
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
250
|
+
def transition_to_color(config, color)
|
|
251
|
+
case color
|
|
252
|
+
when Domain::Color::GREEN
|
|
253
|
+
transition_to_green(config)
|
|
254
|
+
when Domain::Color::YELLOW
|
|
255
|
+
transition_to_yellow(config)
|
|
256
|
+
when Domain::Color::RED
|
|
257
|
+
transition_to_red(config)
|
|
258
|
+
else
|
|
259
|
+
raise ArgumentError, "Invalid color: #{color}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Transitions to GREEN state and ensures only one notification
|
|
264
|
+
#
|
|
265
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
266
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
267
|
+
private def transition_to_green(config)
|
|
268
|
+
current_ts = current_time.to_f
|
|
269
|
+
meta_key = metadata_key(config)
|
|
270
|
+
|
|
271
|
+
became_green = @redis.then do |client|
|
|
272
|
+
client.evalsha(
|
|
273
|
+
transition_to_green_sha,
|
|
274
|
+
argv: [current_ts],
|
|
275
|
+
keys: [meta_key]
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
became_green == 1
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
282
|
+
#
|
|
283
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
284
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
285
|
+
private def transition_to_yellow(config)
|
|
286
|
+
current_ts = current_time.to_i
|
|
287
|
+
meta_key = metadata_key(config)
|
|
288
|
+
|
|
289
|
+
became_yellow = @redis.then do |client|
|
|
290
|
+
client.evalsha(
|
|
291
|
+
transition_to_yellow_sha,
|
|
292
|
+
argv: [current_ts],
|
|
293
|
+
keys: [meta_key]
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
became_yellow == 1
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Transitions to RED state and ensures only one notification
|
|
300
|
+
#
|
|
301
|
+
# @param config [Stoplight::Domain::Config] The light configuration
|
|
302
|
+
# @return [Boolean] true if this is the first instance to detect this transition
|
|
303
|
+
private def transition_to_red(config)
|
|
304
|
+
current_ts = current_time.to_i
|
|
305
|
+
meta_key = metadata_key(config)
|
|
306
|
+
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
307
|
+
|
|
308
|
+
became_red = @redis.then do |client|
|
|
309
|
+
client.evalsha(
|
|
310
|
+
transition_to_red_sha,
|
|
311
|
+
argv: [current_ts, recovery_scheduled_after_ts],
|
|
312
|
+
keys: [meta_key]
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
became_red == 1
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# @param failure_json [String]
|
|
320
|
+
# @return [Domain::Failure]
|
|
321
|
+
private def deserialize_failure(failure_json)
|
|
322
|
+
object = JSON.parse(failure_json)
|
|
323
|
+
error_object = object["error"]
|
|
324
|
+
|
|
325
|
+
error_class = error_object["class"]
|
|
326
|
+
error_message = error_object["message"]
|
|
327
|
+
time = Time.at(object["time"])
|
|
328
|
+
|
|
329
|
+
Domain::Failure.new(error_class, error_message, time)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @param failure [Domain::Failure]
|
|
333
|
+
# @return [String]
|
|
334
|
+
private def serialize_failure(failure)
|
|
335
|
+
JSON.generate(
|
|
336
|
+
{
|
|
337
|
+
error: {
|
|
338
|
+
class: failure.error_class,
|
|
339
|
+
message: failure.error_message
|
|
340
|
+
},
|
|
341
|
+
time: failure.time.to_f
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def_delegator "self.class", :key
|
|
347
|
+
|
|
348
|
+
private def failure_bucket_keys(config, window_end:)
|
|
349
|
+
self.class.buckets_for_window(
|
|
350
|
+
config.name,
|
|
351
|
+
metric: "failure",
|
|
352
|
+
window_end: window_end,
|
|
353
|
+
window_size: config.window_size
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
private def success_bucket_keys(config, window_end:)
|
|
358
|
+
self.class.buckets_for_window(
|
|
359
|
+
config.name,
|
|
360
|
+
metric: "success",
|
|
361
|
+
window_end: window_end,
|
|
362
|
+
window_size: config.window_size
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private def recovery_probe_failure_bucket_keys(config, window_end:)
|
|
367
|
+
self.class.buckets_for_window(
|
|
368
|
+
config.name,
|
|
369
|
+
metric: "recovery_probe_failure",
|
|
370
|
+
window_end: window_end,
|
|
371
|
+
window_size: config.cool_off_time
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
private def recovery_probe_success_bucket_keys(config, window_end:)
|
|
376
|
+
self.class.buckets_for_window(
|
|
377
|
+
config.name,
|
|
378
|
+
metric: "recovery_probe_success",
|
|
379
|
+
window_end: window_end,
|
|
380
|
+
window_size: config.cool_off_time
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private def successes_key(config, time:)
|
|
385
|
+
self.class.bucket_key(config.name, metric: "success", time:)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private def errors_key(config, time:)
|
|
389
|
+
self.class.bucket_key(config.name, metric: "failure", time:)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private def recovery_probe_successes_key(config, time:)
|
|
393
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_success", time:)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
private def recovery_probe_errors_key(config, time:)
|
|
397
|
+
self.class.bucket_key(config.name, metric: "recovery_probe_failure", time:)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
private def metadata_key(config)
|
|
401
|
+
key("metadata", config.name)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
METRICS_TTL = 86400 # 1 day
|
|
405
|
+
private_constant :METRICS_TTL
|
|
406
|
+
|
|
407
|
+
private def metrics_ttl
|
|
408
|
+
METRICS_TTL
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
METADATA_TTL = 86400 * 7 # 7 days
|
|
412
|
+
private_constant :METADATA_TTL
|
|
413
|
+
|
|
414
|
+
private def metadata_ttl
|
|
415
|
+
METADATA_TTL
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
SKEW_TOLERANCE = 5 # seconds
|
|
419
|
+
private_constant :SKEW_TOLERANCE
|
|
420
|
+
|
|
421
|
+
private def detect_clock_skew
|
|
422
|
+
return unless @warn_on_clock_skew
|
|
423
|
+
return unless should_sample?(0.01) # 1% chance
|
|
424
|
+
|
|
425
|
+
redis_seconds, _redis_millis = @redis.then(&:time)
|
|
426
|
+
app_seconds = current_time.to_i
|
|
427
|
+
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
|
428
|
+
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")
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private def should_sample?(probability)
|
|
433
|
+
rand <= probability
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
private def record_success_sha
|
|
437
|
+
@record_success_sha ||= @redis.then do |client|
|
|
438
|
+
client.script("load", Lua::RECORD_SUCCESS)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
private def get_metadata_sha
|
|
443
|
+
@get_metadata_sha ||= @redis.then do |client|
|
|
444
|
+
client.script("load", Lua::GET_METADATA)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
private def transition_to_yellow_sha
|
|
449
|
+
@transition_to_yellow_sha ||= @redis.then do |client|
|
|
450
|
+
client.script("load", Lua::TRANSITION_TO_YELLOW)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
private def transition_to_red_sha
|
|
455
|
+
@transition_to_red_sha ||= @redis.then do |client|
|
|
456
|
+
client.script("load", Lua::TRANSITION_TO_RED)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
private def transition_to_green_sha
|
|
461
|
+
@transition_to_green_sha ||= @redis.then do |client|
|
|
462
|
+
client.script("load", Lua::TRANSITION_TO_GREEN)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
private def record_failure_sha
|
|
467
|
+
@record_failure_sha ||= @redis.then do |client|
|
|
468
|
+
client.script("load", Lua::RECORD_FAILURE)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
private def current_time
|
|
473
|
+
Time.now
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|