stoplight 5.3.5 → 5.4.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 +15 -0
- data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
- data/lib/stoplight/config/system_config.rb +4 -1
- data/lib/stoplight/data_store/fail_safe.rb +41 -42
- data/lib/stoplight/data_store/memory/sliding_window.rb +77 -0
- data/lib/stoplight/data_store/memory.rb +54 -76
- data/lib/stoplight/data_store/redis.rb +27 -29
- data/lib/stoplight/error.rb +30 -1
- data/lib/stoplight/light/config.rb +1 -0
- data/lib/stoplight/light/green_run_strategy.rb +2 -1
- data/lib/stoplight/light/red_run_strategy.rb +7 -2
- data/lib/stoplight/light/run_strategy.rb +3 -1
- data/lib/stoplight/light/yellow_run_strategy.rb +16 -2
- data/lib/stoplight/light.rb +11 -10
- data/lib/stoplight/metadata.rb +7 -5
- data/lib/stoplight/traffic_control/consecutive_errors.rb +2 -2
- data/lib/stoplight/traffic_control/error_rate.rb +2 -2
- data/lib/stoplight/version.rb +1 -1
- metadata +2 -2
- data/lib/stoplight/empty_metadata.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2629a7cdd83dce508860ffe0c7fe3da4cad02eab605e0cb1b4f1e4c0f7db509d
|
|
4
|
+
data.tar.gz: 50f13d3625ab327dd07815fd3da1a9384a0cf0f1d75af2db853b098c3b90ba2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9aa893f78f602c6d71f82f15deb41d6aa0bb4bc0ca37bf3756003699e2b158e1d3a857ee0f1fdd6cebcbb35fb787e5f515a36f89dd5560c000297302cd0e7e18
|
|
7
|
+
data.tar.gz: 772938b653645a1ad3c8ca5e9e5958871c1b8802da6ce947d38fd9d5390fa9e336d8838941571569aed102cd0656c58389224b417bcf059f4569295c42894d6d
|
data/README.md
CHANGED
|
@@ -99,6 +99,19 @@ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
|
|
|
99
99
|
light.color # => "red"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
The `Stoplight::Error::RedLight` provides metadata about the error:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
def run_request
|
|
106
|
+
light = Stoplight("Example", cool_off_time: 10)
|
|
107
|
+
light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight
|
|
108
|
+
rescue Stoplight::Error::RedLight => error
|
|
109
|
+
puts error.light_name #=> "Example"
|
|
110
|
+
puts error.cool_off_time #=> 10
|
|
111
|
+
puts error.retry_after #=> Absolute Time after which a recovery attempt can occur (e.g., "2025-10-21 15:39:50.672414 +0600")
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
102
115
|
After one minute, the light transitions to yellow, allowing a test execution:
|
|
103
116
|
|
|
104
117
|
```ruby
|
|
@@ -131,6 +144,8 @@ receives `nil`. In both cases, the return value of the fallback becomes the retu
|
|
|
131
144
|
|
|
132
145
|
Stoplight comes with a built-in Admin Panel that can track all active Lights and manually lock them in the desired state (`Green` or `Red`). Locking lights in certain states might be helpful in scenarios like E2E testing.
|
|
133
146
|
|
|
147
|
+

|
|
148
|
+
|
|
134
149
|
To add Admin Panel protected by basic authentication to your Rails project, add this configuration to your `config/routes.rb` file.
|
|
135
150
|
|
|
136
151
|
```ruby
|
|
@@ -12,7 +12,9 @@ require "redis"
|
|
|
12
12
|
# "redis://admin:p4ssw0rd@10.0.1.1:6380/15"
|
|
13
13
|
redis = Redis.new
|
|
14
14
|
data_store = Stoplight::DataStore::Redis.new(redis)
|
|
15
|
+
error_notifier = Rails.error.method(:report)
|
|
15
16
|
|
|
16
17
|
Stoplight.configure do |config|
|
|
17
18
|
config.data_store = data_store
|
|
19
|
+
config.error_notifier = error_notifier
|
|
18
20
|
end
|
|
@@ -10,10 +10,6 @@ module Stoplight
|
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
12
|
class FailSafe < Base
|
|
13
|
-
# @!attribute [r] data_store
|
|
14
|
-
# @return [Stoplight::DataStore::Base] The underlying data store being wrapped.
|
|
15
|
-
protected attr_reader :data_store
|
|
16
|
-
|
|
17
13
|
class << self
|
|
18
14
|
# Wraps a data store with fail-safe mechanisms.
|
|
19
15
|
#
|
|
@@ -30,62 +26,70 @@ module Stoplight
|
|
|
30
26
|
end
|
|
31
27
|
end
|
|
32
28
|
|
|
29
|
+
# @!attribute data_store
|
|
30
|
+
# @return [Stoplight::DataStore::Base] The underlying primary data store being used
|
|
31
|
+
protected attr_reader :data_store
|
|
32
|
+
|
|
33
|
+
# @!attribute failover_data_store
|
|
34
|
+
# @return [Stoplight::DataStore::Base] The fallback data store used when the primary fails.
|
|
35
|
+
private attr_reader :failover_data_store
|
|
36
|
+
|
|
37
|
+
# @!attribute circuit_breaker
|
|
38
|
+
# @return [Stoplight::Light] The circuit breaker used to handle data store failures.
|
|
39
|
+
private attr_reader :circuit_breaker
|
|
40
|
+
|
|
33
41
|
# @param data_store [Stoplight::DataStore::Base]
|
|
34
|
-
def initialize(data_store)
|
|
42
|
+
def initialize(data_store, failover_data_store: Default::DATA_STORE)
|
|
35
43
|
@data_store = data_store
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
data_store: Default::DATA_STORE,
|
|
39
|
-
traffic_control: TrafficControl::ConsecutiveErrors.new,
|
|
40
|
-
threshold: Default::THRESHOLD
|
|
41
|
-
)
|
|
44
|
+
@failover_data_store = failover_data_store
|
|
45
|
+
@circuit_breaker = Stoplight.system_light("stoplight:data_store:fail_safe:#{data_store.class.name}")
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def names
|
|
45
|
-
with_fallback(
|
|
49
|
+
with_fallback(:names) do
|
|
46
50
|
data_store.names
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
def get_metadata(config)
|
|
51
|
-
with_fallback(
|
|
52
|
-
data_store.get_metadata(config)
|
|
54
|
+
def get_metadata(config, *args, **kwargs)
|
|
55
|
+
with_fallback(:get_metadata, config, *args, **kwargs) do
|
|
56
|
+
data_store.get_metadata(config, *args, **kwargs)
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
59
|
|
|
56
|
-
def record_failure(config,
|
|
57
|
-
with_fallback(
|
|
58
|
-
data_store.record_failure(config,
|
|
60
|
+
def record_failure(config, *args, **kwargs)
|
|
61
|
+
with_fallback(:record_failure, config, *args, **kwargs) do
|
|
62
|
+
data_store.record_failure(config, *args, **kwargs)
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
def record_success(config, **
|
|
63
|
-
with_fallback(
|
|
64
|
-
data_store.record_success(config, **
|
|
66
|
+
def record_success(config, *args, **kwargs)
|
|
67
|
+
with_fallback(:record_success, config, *args, **kwargs) do
|
|
68
|
+
data_store.record_success(config, *args, **kwargs)
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
def record_recovery_probe_success(config, **
|
|
69
|
-
with_fallback(
|
|
70
|
-
data_store.record_recovery_probe_success(config, **
|
|
72
|
+
def record_recovery_probe_success(config, *args, **kwargs)
|
|
73
|
+
with_fallback(:record_recovery_probe_success, config, *args, **kwargs) do
|
|
74
|
+
data_store.record_recovery_probe_success(config, *args, **kwargs)
|
|
71
75
|
end
|
|
72
76
|
end
|
|
73
77
|
|
|
74
|
-
def record_recovery_probe_failure(config,
|
|
75
|
-
with_fallback(
|
|
76
|
-
data_store.record_recovery_probe_failure(config,
|
|
78
|
+
def record_recovery_probe_failure(config, *args, **kwargs)
|
|
79
|
+
with_fallback(:record_recovery_probe_failure, config, *args, **kwargs) do
|
|
80
|
+
data_store.record_recovery_probe_failure(config, *args, **kwargs)
|
|
77
81
|
end
|
|
78
82
|
end
|
|
79
83
|
|
|
80
|
-
def set_state(config,
|
|
81
|
-
with_fallback(
|
|
82
|
-
data_store.set_state(config,
|
|
84
|
+
def set_state(config, *args, **kwargs)
|
|
85
|
+
with_fallback(:set_state, config, *args, **kwargs) do
|
|
86
|
+
data_store.set_state(config, *args, **kwargs)
|
|
83
87
|
end
|
|
84
88
|
end
|
|
85
89
|
|
|
86
|
-
def transition_to_color(config,
|
|
87
|
-
with_fallback(
|
|
88
|
-
data_store.transition_to_color(config,
|
|
90
|
+
def transition_to_color(config, *args, **kwargs)
|
|
91
|
+
with_fallback(:transition_to_color, config, *args, **kwargs) do
|
|
92
|
+
data_store.transition_to_color(config, *args, **kwargs)
|
|
89
93
|
end
|
|
90
94
|
end
|
|
91
95
|
|
|
@@ -93,21 +97,16 @@ module Stoplight
|
|
|
93
97
|
other.is_a?(self.class) && other.data_store == data_store
|
|
94
98
|
end
|
|
95
99
|
|
|
96
|
-
# @param
|
|
97
|
-
|
|
98
|
-
private def with_fallback(default = nil, config = nil, &code)
|
|
100
|
+
# @param method_name [Symbol] protected method name
|
|
101
|
+
private def with_fallback(method_name, *args, **kwargs, &code)
|
|
99
102
|
fallback = proc do |error|
|
|
103
|
+
config = args.first
|
|
100
104
|
config.error_notifier.call(error) if config && error
|
|
101
|
-
|
|
105
|
+
@failover_data_store.public_send(method_name, *args, **kwargs)
|
|
102
106
|
end
|
|
103
107
|
|
|
104
108
|
circuit_breaker.run(fallback, &code)
|
|
105
109
|
end
|
|
106
|
-
|
|
107
|
-
# @return [Stoplight::Light] The circuit breaker used to handle failures.
|
|
108
|
-
private def circuit_breaker
|
|
109
|
-
@circuit_breaker ||= Stoplight.system_light("stoplight:data_store:fail_safe:#{data_store.class.name}")
|
|
110
|
-
end
|
|
111
110
|
end
|
|
112
111
|
end
|
|
113
112
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module DataStore
|
|
5
|
+
class Memory < Base
|
|
6
|
+
# Hash-based sliding window for O(1) amortized operations.
|
|
7
|
+
#
|
|
8
|
+
# Maintains a running sum and stores per-second counts in a Hash. Ruby's Hash
|
|
9
|
+
# preserves insertion order (FIFO), allowing efficient removal of expired
|
|
10
|
+
# buckets from the front via +Hash#shift+, with their counts subtracted from
|
|
11
|
+
# the running sum.
|
|
12
|
+
#
|
|
13
|
+
# Performance: O(1) amortized for both reads and writes
|
|
14
|
+
# Memory: Bounded to the number of buckets
|
|
15
|
+
#
|
|
16
|
+
# @note Not thread-safe; synchronization must be handled externally
|
|
17
|
+
# @api private
|
|
18
|
+
class SlidingWindow
|
|
19
|
+
# @!attribute buckets
|
|
20
|
+
# @return [Hash<Integer, Integer>] A hash mapping time buckets to their counts
|
|
21
|
+
private attr_reader :buckets
|
|
22
|
+
|
|
23
|
+
# @!attribute running_sum
|
|
24
|
+
# @return [Integer] The running sum of all increments in the current window
|
|
25
|
+
private attr_accessor :running_sum
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@buckets = Hash.new { |buckets, bucket| buckets[bucket] = 0 }
|
|
29
|
+
@running_sum = 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Increment the count at a given timestamp
|
|
33
|
+
def increment
|
|
34
|
+
buckets[current_bucket] += 1
|
|
35
|
+
self.running_sum += 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param window_start [Time]
|
|
39
|
+
# @return [Integer]
|
|
40
|
+
def sum_in_window(window_start)
|
|
41
|
+
slide_window!(window_start)
|
|
42
|
+
self.running_sum
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private def slide_window!(window_start)
|
|
46
|
+
window_start_ts = window_start.to_i
|
|
47
|
+
|
|
48
|
+
loop do
|
|
49
|
+
timestamp, sum = buckets.first
|
|
50
|
+
if timestamp.nil? || timestamp >= window_start_ts
|
|
51
|
+
break
|
|
52
|
+
else
|
|
53
|
+
self.running_sum -= sum
|
|
54
|
+
buckets.shift
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private def current_bucket
|
|
60
|
+
bucket_for_time(current_time)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private def bucket_for_time(time)
|
|
64
|
+
time.to_i
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private def current_time
|
|
68
|
+
Time.now
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inspect
|
|
72
|
+
"#<#{self.class.name} #{buckets}>"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -7,14 +7,15 @@ module Stoplight
|
|
|
7
7
|
# @see Base
|
|
8
8
|
class Memory < Base
|
|
9
9
|
include MonitorMixin
|
|
10
|
+
|
|
10
11
|
KEY_SEPARATOR = ":"
|
|
11
12
|
|
|
12
13
|
def initialize
|
|
13
|
-
@errors = Hash.new { |
|
|
14
|
-
@successes = Hash.new { |
|
|
14
|
+
@errors = Hash.new { |errors, light_name| errors[light_name] = SlidingWindow.new }
|
|
15
|
+
@successes = Hash.new { |successes, light_name| successes[light_name] = SlidingWindow.new }
|
|
15
16
|
|
|
16
|
-
@recovery_probe_errors = Hash.new { |
|
|
17
|
-
@recovery_probe_successes = Hash.new { |
|
|
17
|
+
@recovery_probe_errors = Hash.new { |recovery_probe_errors, light_name| recovery_probe_errors[light_name] = SlidingWindow.new }
|
|
18
|
+
@recovery_probe_successes = Hash.new { |recovery_probe_successes, light_name| recovery_probe_successes[light_name] = SlidingWindow.new }
|
|
18
19
|
|
|
19
20
|
@metadata = Hash.new { |h, k| h[k] = Metadata.new }
|
|
20
21
|
super # MonitorMixin
|
|
@@ -29,66 +30,41 @@ module Stoplight
|
|
|
29
30
|
# @return [Stoplight::Metadata]
|
|
30
31
|
def get_metadata(config)
|
|
31
32
|
light_name = config.name
|
|
32
|
-
window_end = Time.now
|
|
33
|
-
recovery_window = (window_end - config.cool_off_time + 1)..window_end
|
|
34
33
|
|
|
35
34
|
synchronize do
|
|
35
|
+
current_time = self.current_time
|
|
36
|
+
recovery_window_start = (current_time - config.cool_off_time)
|
|
36
37
|
recovered_at = @metadata[light_name].recovered_at
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
(window_start..window_end)
|
|
38
|
+
window_start = if config.window_size
|
|
39
|
+
[recovered_at, (current_time - config.window_size)].compact.max
|
|
40
40
|
else
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
errors = @errors[config.name].count do |request_time|
|
|
45
|
-
window.cover?(request_time)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
successes = @successes[config.name].count do |request_time|
|
|
49
|
-
window.cover?(request_time)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
recovery_probe_errors = @recovery_probe_errors[config.name].count do |request_time|
|
|
53
|
-
recovery_window.cover?(request_time)
|
|
54
|
-
end
|
|
55
|
-
recovery_probe_successes = @recovery_probe_successes[config.name].count do |request_time|
|
|
56
|
-
recovery_window.cover?(request_time)
|
|
41
|
+
current_time
|
|
57
42
|
end
|
|
58
43
|
|
|
59
44
|
@metadata[light_name].with(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
current_time:,
|
|
46
|
+
errors: @errors[config.name].sum_in_window(window_start),
|
|
47
|
+
successes: @successes[config.name].sum_in_window(window_start),
|
|
48
|
+
recovery_probe_errors: @recovery_probe_errors[config.name].sum_in_window(recovery_window_start),
|
|
49
|
+
recovery_probe_successes: @recovery_probe_successes[config.name].sum_in_window(recovery_window_start)
|
|
64
50
|
)
|
|
65
51
|
end
|
|
66
52
|
end
|
|
67
53
|
|
|
68
|
-
# @param metrics [<Time>]
|
|
69
|
-
# @param window_size [Numeric, nil]
|
|
70
|
-
# @return [void]
|
|
71
|
-
def cleanup(metrics, window_size:)
|
|
72
|
-
min_age = Time.now - [window_size&.*(3), METRICS_RETENTION_TIME].compact.min
|
|
73
|
-
|
|
74
|
-
metrics.reject! { _1 < min_age }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
54
|
# @param config [Stoplight::Light::Config]
|
|
78
55
|
# @param failure [Stoplight::Failure]
|
|
79
56
|
# @return [Stoplight::Metadata]
|
|
80
57
|
def record_failure(config, failure)
|
|
58
|
+
current_time = self.current_time
|
|
81
59
|
light_name = config.name
|
|
82
60
|
|
|
83
61
|
synchronize do
|
|
84
|
-
@errors[light_name].
|
|
85
|
-
|
|
86
|
-
cleanup(@errors[light_name], window_size: config.window_size)
|
|
62
|
+
@errors[light_name].increment if config.window_size
|
|
87
63
|
|
|
88
64
|
metadata = @metadata[light_name]
|
|
89
|
-
@metadata[light_name] = if metadata.last_error_at.nil? ||
|
|
65
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
|
|
90
66
|
metadata.with(
|
|
91
|
-
last_error_at:
|
|
67
|
+
last_error_at: current_time,
|
|
92
68
|
last_error: failure,
|
|
93
69
|
consecutive_errors: metadata.consecutive_errors.succ,
|
|
94
70
|
consecutive_successes: 0
|
|
@@ -104,20 +80,18 @@ module Stoplight
|
|
|
104
80
|
end
|
|
105
81
|
|
|
106
82
|
# @param config [Stoplight::Light::Config]
|
|
107
|
-
# @param request_id [String]
|
|
108
|
-
# @param request_time [Time]
|
|
109
83
|
# @return [void]
|
|
110
|
-
def record_success(config
|
|
84
|
+
def record_success(config)
|
|
111
85
|
light_name = config.name
|
|
86
|
+
current_time = self.current_time
|
|
112
87
|
|
|
113
88
|
synchronize do
|
|
114
|
-
@successes[light_name].
|
|
115
|
-
cleanup(@successes[light_name], window_size: config.window_size)
|
|
89
|
+
@successes[light_name].increment if config.window_size
|
|
116
90
|
|
|
117
91
|
metadata = @metadata[light_name]
|
|
118
|
-
@metadata[light_name] = if metadata.last_success_at.nil? ||
|
|
92
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
|
|
119
93
|
metadata.with(
|
|
120
|
-
last_success_at:
|
|
94
|
+
last_success_at: current_time,
|
|
121
95
|
consecutive_errors: 0,
|
|
122
96
|
consecutive_successes: metadata.consecutive_successes.succ
|
|
123
97
|
)
|
|
@@ -135,15 +109,15 @@ module Stoplight
|
|
|
135
109
|
# @return [Stoplight::Metadata]
|
|
136
110
|
def record_recovery_probe_failure(config, failure)
|
|
137
111
|
light_name = config.name
|
|
112
|
+
current_time = self.current_time
|
|
138
113
|
|
|
139
114
|
synchronize do
|
|
140
|
-
@recovery_probe_errors[light_name].
|
|
141
|
-
cleanup(@recovery_probe_errors[light_name], window_size: config.cool_off_time)
|
|
115
|
+
@recovery_probe_errors[light_name].increment
|
|
142
116
|
|
|
143
117
|
metadata = @metadata[light_name]
|
|
144
|
-
@metadata[light_name] = if metadata.last_error_at.nil? ||
|
|
118
|
+
@metadata[light_name] = if metadata.last_error_at.nil? || current_time > metadata.last_error_at
|
|
145
119
|
metadata.with(
|
|
146
|
-
last_error_at:
|
|
120
|
+
last_error_at: current_time,
|
|
147
121
|
last_error: failure,
|
|
148
122
|
consecutive_errors: metadata.consecutive_errors.succ,
|
|
149
123
|
consecutive_successes: 0
|
|
@@ -159,28 +133,23 @@ module Stoplight
|
|
|
159
133
|
end
|
|
160
134
|
|
|
161
135
|
# @param config [Stoplight::Light::Config]
|
|
162
|
-
# @param request_id [String]
|
|
163
|
-
# @param request_time [Time]
|
|
164
136
|
# @return [Stoplight::Metadata]
|
|
165
|
-
def record_recovery_probe_success(config
|
|
137
|
+
def record_recovery_probe_success(config)
|
|
166
138
|
light_name = config.name
|
|
139
|
+
current_time = self.current_time
|
|
167
140
|
|
|
168
141
|
synchronize do
|
|
169
|
-
@recovery_probe_successes[light_name].
|
|
170
|
-
cleanup(@recovery_probe_successes[light_name], window_size: config.cool_off_time)
|
|
142
|
+
@recovery_probe_successes[light_name].increment
|
|
171
143
|
|
|
172
144
|
metadata = @metadata[light_name]
|
|
173
|
-
|
|
174
|
-
@metadata[light_name] = if metadata.last_success_at.nil? || request_time > metadata.last_success_at
|
|
145
|
+
@metadata[light_name] = if metadata.last_success_at.nil? || current_time > metadata.last_success_at
|
|
175
146
|
metadata.with(
|
|
176
|
-
last_success_at:
|
|
177
|
-
recovery_started_at:,
|
|
147
|
+
last_success_at: current_time,
|
|
178
148
|
consecutive_errors: 0,
|
|
179
149
|
consecutive_successes: metadata.consecutive_successes.succ
|
|
180
150
|
)
|
|
181
151
|
else
|
|
182
152
|
metadata.with(
|
|
183
|
-
recovery_started_at:,
|
|
184
153
|
consecutive_errors: 0,
|
|
185
154
|
consecutive_successes: metadata.consecutive_successes.succ
|
|
186
155
|
)
|
|
@@ -211,16 +180,15 @@ module Stoplight
|
|
|
211
180
|
#
|
|
212
181
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
213
182
|
# @param color [String] The color to transition to ("GREEN", "YELLOW", or "RED")
|
|
214
|
-
# @param current_time [Time]
|
|
215
183
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
216
|
-
def transition_to_color(config, color
|
|
184
|
+
def transition_to_color(config, color)
|
|
217
185
|
case color
|
|
218
186
|
when Color::GREEN
|
|
219
187
|
transition_to_green(config)
|
|
220
188
|
when Color::YELLOW
|
|
221
|
-
transition_to_yellow(config
|
|
189
|
+
transition_to_yellow(config)
|
|
222
190
|
when Color::RED
|
|
223
|
-
transition_to_red(config
|
|
191
|
+
transition_to_red(config)
|
|
224
192
|
else
|
|
225
193
|
raise ArgumentError, "Invalid color: #{color}"
|
|
226
194
|
end
|
|
@@ -230,8 +198,9 @@ module Stoplight
|
|
|
230
198
|
#
|
|
231
199
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
232
200
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
233
|
-
private def transition_to_green(config
|
|
201
|
+
private def transition_to_green(config)
|
|
234
202
|
light_name = config.name
|
|
203
|
+
current_time = self.current_time
|
|
235
204
|
|
|
236
205
|
synchronize do
|
|
237
206
|
metadata = @metadata[light_name]
|
|
@@ -252,16 +221,14 @@ module Stoplight
|
|
|
252
221
|
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
253
222
|
#
|
|
254
223
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
255
|
-
# @param current_time [Time]
|
|
256
224
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
257
|
-
private def transition_to_yellow(config
|
|
225
|
+
private def transition_to_yellow(config)
|
|
258
226
|
light_name = config.name
|
|
227
|
+
current_time = self.current_time
|
|
259
228
|
|
|
260
229
|
synchronize do
|
|
261
230
|
metadata = @metadata[light_name]
|
|
262
|
-
if metadata.recovery_started_at
|
|
263
|
-
false
|
|
264
|
-
else
|
|
231
|
+
if metadata.recovery_started_at.nil?
|
|
265
232
|
@metadata[light_name] = metadata.with(
|
|
266
233
|
recovery_started_at: current_time,
|
|
267
234
|
recovery_scheduled_after: nil,
|
|
@@ -269,6 +236,13 @@ module Stoplight
|
|
|
269
236
|
breached_at: nil
|
|
270
237
|
)
|
|
271
238
|
true
|
|
239
|
+
else
|
|
240
|
+
@metadata[light_name] = metadata.with(
|
|
241
|
+
recovery_scheduled_after: nil,
|
|
242
|
+
recovered_at: nil,
|
|
243
|
+
breached_at: nil
|
|
244
|
+
)
|
|
245
|
+
false
|
|
272
246
|
end
|
|
273
247
|
end
|
|
274
248
|
end
|
|
@@ -276,10 +250,10 @@ module Stoplight
|
|
|
276
250
|
# Transitions to RED state and ensures only one notification
|
|
277
251
|
#
|
|
278
252
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
279
|
-
# @param current_time [Time]
|
|
280
253
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
281
|
-
private def transition_to_red(config
|
|
254
|
+
private def transition_to_red(config)
|
|
282
255
|
light_name = config.name
|
|
256
|
+
current_time = self.current_time
|
|
283
257
|
recovery_scheduled_after = current_time + config.cool_off_time
|
|
284
258
|
|
|
285
259
|
synchronize do
|
|
@@ -302,6 +276,10 @@ module Stoplight
|
|
|
302
276
|
end
|
|
303
277
|
end
|
|
304
278
|
end
|
|
279
|
+
|
|
280
|
+
private def current_time
|
|
281
|
+
Time.now
|
|
282
|
+
end
|
|
305
283
|
end
|
|
306
284
|
end
|
|
307
285
|
end
|
|
@@ -94,8 +94,7 @@ module Stoplight
|
|
|
94
94
|
def get_metadata(config)
|
|
95
95
|
detect_clock_skew
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
window_end_ts = window_end.to_i
|
|
97
|
+
window_end_ts = current_time.to_i
|
|
99
98
|
window_start_ts = window_end_ts - [config.window_size, Base::METRICS_RETENTION_TIME].compact.min.to_i
|
|
100
99
|
recovery_window_start_ts = window_end_ts - config.cool_off_time.to_i
|
|
101
100
|
|
|
@@ -133,10 +132,11 @@ module Stoplight
|
|
|
133
132
|
last_error = normalize_failure(last_error_json, config.error_notifier) if last_error_json
|
|
134
133
|
|
|
135
134
|
Metadata.new(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
current_time:,
|
|
136
|
+
successes:,
|
|
137
|
+
errors:,
|
|
138
|
+
recovery_probe_successes:,
|
|
139
|
+
recovery_probe_errors:,
|
|
140
140
|
last_error:,
|
|
141
141
|
**meta_hash
|
|
142
142
|
)
|
|
@@ -146,7 +146,7 @@ module Stoplight
|
|
|
146
146
|
# @param failure [Stoplight::Failure] The failure to record.
|
|
147
147
|
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
|
148
148
|
def record_failure(config, failure)
|
|
149
|
-
current_ts =
|
|
149
|
+
current_ts = current_time.to_i
|
|
150
150
|
failure_json = failure.to_json
|
|
151
151
|
|
|
152
152
|
@redis.then do |client|
|
|
@@ -162,16 +162,16 @@ module Stoplight
|
|
|
162
162
|
get_metadata(config)
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
def record_success(config, request_id: SecureRandom.hex(12)
|
|
166
|
-
|
|
165
|
+
def record_success(config, request_id: SecureRandom.hex(12))
|
|
166
|
+
current_ts = current_time.to_i
|
|
167
167
|
|
|
168
168
|
@redis.then do |client|
|
|
169
169
|
client.evalsha(
|
|
170
170
|
record_success_sha,
|
|
171
|
-
argv: [
|
|
171
|
+
argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
|
|
172
172
|
keys: [
|
|
173
173
|
metadata_key(config),
|
|
174
|
-
config.window_size && successes_key(config, time:
|
|
174
|
+
config.window_size && successes_key(config, time: current_ts)
|
|
175
175
|
].compact
|
|
176
176
|
)
|
|
177
177
|
end
|
|
@@ -183,7 +183,7 @@ module Stoplight
|
|
|
183
183
|
# @param failure [Failure] The failure to record.
|
|
184
184
|
# @return [Stoplight::Metadata] The updated metadata after recording the failure.
|
|
185
185
|
def record_recovery_probe_failure(config, failure)
|
|
186
|
-
current_ts =
|
|
186
|
+
current_ts = current_time.to_i
|
|
187
187
|
failure_json = failure.to_json
|
|
188
188
|
|
|
189
189
|
@redis.then do |client|
|
|
@@ -203,18 +203,17 @@ module Stoplight
|
|
|
203
203
|
#
|
|
204
204
|
# @param config [Stoplight::Light::Config] The light configuration.
|
|
205
205
|
# @param request_id [String] The unique identifier for the request
|
|
206
|
-
# @param request_time [Time] The time of the request
|
|
207
206
|
# @return [Stoplight::Metadata] The updated metadata after recording the success.
|
|
208
|
-
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12)
|
|
209
|
-
|
|
207
|
+
def record_recovery_probe_success(config, request_id: SecureRandom.hex(12))
|
|
208
|
+
current_ts = current_time.to_i
|
|
210
209
|
|
|
211
210
|
@redis.then do |client|
|
|
212
211
|
client.evalsha(
|
|
213
212
|
record_success_sha,
|
|
214
|
-
argv: [
|
|
213
|
+
argv: [current_ts, request_id, metrics_ttl, metadata_ttl],
|
|
215
214
|
keys: [
|
|
216
215
|
metadata_key(config),
|
|
217
|
-
recovery_probe_successes_key(config, time:
|
|
216
|
+
recovery_probe_successes_key(config, time: current_ts)
|
|
218
217
|
].compact
|
|
219
218
|
)
|
|
220
219
|
end
|
|
@@ -236,18 +235,15 @@ module Stoplight
|
|
|
236
235
|
#
|
|
237
236
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
238
237
|
# @param color [String] The color to transition to ("green", "yellow", or "red")
|
|
239
|
-
# @param current_time [Time] Current timestamp
|
|
240
238
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
241
|
-
def transition_to_color(config, color
|
|
242
|
-
current_time.to_i
|
|
243
|
-
|
|
239
|
+
def transition_to_color(config, color)
|
|
244
240
|
case color
|
|
245
241
|
when Color::GREEN
|
|
246
242
|
transition_to_green(config)
|
|
247
243
|
when Color::YELLOW
|
|
248
|
-
transition_to_yellow(config
|
|
244
|
+
transition_to_yellow(config)
|
|
249
245
|
when Color::RED
|
|
250
|
-
transition_to_red(config
|
|
246
|
+
transition_to_red(config)
|
|
251
247
|
else
|
|
252
248
|
raise ArgumentError, "Invalid color: #{color}"
|
|
253
249
|
end
|
|
@@ -257,7 +253,7 @@ module Stoplight
|
|
|
257
253
|
#
|
|
258
254
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
259
255
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
260
|
-
private def transition_to_green(config
|
|
256
|
+
private def transition_to_green(config)
|
|
261
257
|
current_ts = current_time.to_i
|
|
262
258
|
meta_key = metadata_key(config)
|
|
263
259
|
|
|
@@ -274,9 +270,8 @@ module Stoplight
|
|
|
274
270
|
# Transitions to YELLOW (recovery) state and ensures only one notification
|
|
275
271
|
#
|
|
276
272
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
277
|
-
# @param current_time [Time] Current timestamp
|
|
278
273
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
279
|
-
private def transition_to_yellow(config
|
|
274
|
+
private def transition_to_yellow(config)
|
|
280
275
|
current_ts = current_time.to_i
|
|
281
276
|
meta_key = metadata_key(config)
|
|
282
277
|
|
|
@@ -293,9 +288,8 @@ module Stoplight
|
|
|
293
288
|
# Transitions to RED state and ensures only one notification
|
|
294
289
|
#
|
|
295
290
|
# @param config [Stoplight::Light::Config] The light configuration
|
|
296
|
-
# @param current_time [Time] Current timestamp
|
|
297
291
|
# @return [Boolean] true if this is the first instance to detect this transition
|
|
298
|
-
private def transition_to_red(config
|
|
292
|
+
private def transition_to_red(config)
|
|
299
293
|
current_ts = current_time.to_i
|
|
300
294
|
meta_key = metadata_key(config)
|
|
301
295
|
recovery_scheduled_after_ts = current_ts + config.cool_off_time
|
|
@@ -398,7 +392,7 @@ module Stoplight
|
|
|
398
392
|
return unless should_sample?(0.01) # 1% chance
|
|
399
393
|
|
|
400
394
|
redis_seconds, _redis_millis = @redis.then(&:time)
|
|
401
|
-
app_seconds =
|
|
395
|
+
app_seconds = current_time.to_i
|
|
402
396
|
if (redis_seconds - app_seconds).abs > SKEW_TOLERANCE
|
|
403
397
|
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")
|
|
404
398
|
end
|
|
@@ -443,6 +437,10 @@ module Stoplight
|
|
|
443
437
|
client.script("load", Lua::RECORD_FAILURE)
|
|
444
438
|
end
|
|
445
439
|
end
|
|
440
|
+
|
|
441
|
+
private def current_time
|
|
442
|
+
Time.now
|
|
443
|
+
end
|
|
446
444
|
end
|
|
447
445
|
end
|
|
448
446
|
end
|
data/lib/stoplight/error.rb
CHANGED
|
@@ -5,6 +5,35 @@ module Stoplight
|
|
|
5
5
|
Base = Class.new(StandardError)
|
|
6
6
|
ConfigurationError = Class.new(Base)
|
|
7
7
|
IncorrectColor = Class.new(Base)
|
|
8
|
-
RedLight
|
|
8
|
+
class RedLight < Base
|
|
9
|
+
# @!attribute light_name
|
|
10
|
+
# @return [String] The light's name
|
|
11
|
+
attr_reader :light_name
|
|
12
|
+
|
|
13
|
+
# @!attribute cool_off_time
|
|
14
|
+
# @return [Numeric] Cool-off period in seconds
|
|
15
|
+
attr_reader :cool_off_time
|
|
16
|
+
|
|
17
|
+
# @!attribute retry_after
|
|
18
|
+
# @return [Time] Absolute Time after which a recovery attempt can occur
|
|
19
|
+
attr_reader :retry_after
|
|
20
|
+
|
|
21
|
+
# Initializes a new RedLight error.
|
|
22
|
+
#
|
|
23
|
+
# @param light_name [String] The light's name
|
|
24
|
+
#
|
|
25
|
+
# @option cool_off_time [Numeric] Cool-off period in seconds
|
|
26
|
+
#
|
|
27
|
+
# @option retry_after [Time] Absolute Time after which a recovery attempt can occur
|
|
28
|
+
#
|
|
29
|
+
# @return [Stoplight::Error::RedLight]
|
|
30
|
+
def initialize(light_name, cool_off_time:, retry_after:)
|
|
31
|
+
@light_name = light_name
|
|
32
|
+
@cool_off_time = cool_off_time
|
|
33
|
+
@retry_after = retry_after
|
|
34
|
+
|
|
35
|
+
super(light_name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
9
38
|
end
|
|
10
39
|
end
|
|
@@ -12,10 +12,11 @@ module Stoplight
|
|
|
12
12
|
# Executes the provided code block when the light is in the green state.
|
|
13
13
|
#
|
|
14
14
|
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
15
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
|
15
16
|
# @yield The code block to execute.
|
|
16
17
|
# @return [Object] The result of the code block if successful.
|
|
17
18
|
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
18
|
-
def execute(fallback, &code)
|
|
19
|
+
def execute(fallback, metadata:, &code)
|
|
19
20
|
# TODO: Consider implementing sampling rate to limit the memory footprint
|
|
20
21
|
code.call.tap { record_success }
|
|
21
22
|
rescue => error
|
|
@@ -12,13 +12,18 @@ module Stoplight
|
|
|
12
12
|
# Executes the fallback proc when the light is in the red state.
|
|
13
13
|
#
|
|
14
14
|
# @param fallback [Proc, nil] A fallback proc to execute instead of the code block.
|
|
15
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
|
15
16
|
# @return [Object, nil] The result of the fallback proc if provided.
|
|
16
17
|
# @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
|
|
17
|
-
def execute(fallback)
|
|
18
|
+
def execute(fallback, metadata:)
|
|
18
19
|
if fallback
|
|
19
20
|
fallback.call(nil)
|
|
20
21
|
else
|
|
21
|
-
raise Error::RedLight
|
|
22
|
+
raise Error::RedLight.new(
|
|
23
|
+
config.name,
|
|
24
|
+
cool_off_time: config.cool_off_time,
|
|
25
|
+
retry_after: metadata.recovery_scheduled_after
|
|
26
|
+
)
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
end
|
|
@@ -22,7 +22,9 @@ module Stoplight
|
|
|
22
22
|
@data_store = config.data_store
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
26
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
|
27
|
+
def execute(fallback, metadata:, &code)
|
|
26
28
|
raise NotImplementedError, "Subclasses must implement the execute method"
|
|
27
29
|
end
|
|
28
30
|
end
|
|
@@ -13,10 +13,12 @@ module Stoplight
|
|
|
13
13
|
# Executes the provided code block when the light is in the yellow state.
|
|
14
14
|
#
|
|
15
15
|
# @param fallback [Proc, nil] A fallback proc to execute in case of an error.
|
|
16
|
+
# @param metadata [Stoplight::Metadata] Metadata capturing the current state of the light.
|
|
16
17
|
# @yield The code block to execute.
|
|
17
18
|
# @return [Object] The result of the code block if successful.
|
|
18
19
|
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
19
|
-
def execute(fallback, &code)
|
|
20
|
+
def execute(fallback, metadata:, &code)
|
|
21
|
+
transition_to_yellow(metadata:)
|
|
20
22
|
# TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
|
|
21
23
|
code.call.tap { record_recovery_probe_success }
|
|
22
24
|
rescue => error
|
|
@@ -47,6 +49,18 @@ module Stoplight
|
|
|
47
49
|
recover(metadata)
|
|
48
50
|
end
|
|
49
51
|
|
|
52
|
+
# @param metadata [Stoplight::Metadata]
|
|
53
|
+
# @return [void]
|
|
54
|
+
def transition_to_yellow(metadata:)
|
|
55
|
+
return unless metadata.color == Color::YELLOW
|
|
56
|
+
|
|
57
|
+
if metadata.recovery_scheduled_after && config.data_store.transition_to_color(config, Color::YELLOW)
|
|
58
|
+
config.notifiers.each do |notifier|
|
|
59
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
50
64
|
private def recover(metadata)
|
|
51
65
|
recovery_result = config.traffic_recovery.determine_color(config, metadata)
|
|
52
66
|
|
|
@@ -60,7 +74,7 @@ module Stoplight
|
|
|
60
74
|
when TrafficRecovery::YELLOW
|
|
61
75
|
if data_store.transition_to_color(config, Color::YELLOW)
|
|
62
76
|
config.notifiers.each do |notifier|
|
|
63
|
-
notifier.notify(config, Color::
|
|
77
|
+
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
|
64
78
|
end
|
|
65
79
|
end
|
|
66
80
|
when TrafficRecovery::RED
|
data/lib/stoplight/light.rb
CHANGED
|
@@ -32,10 +32,7 @@ module Stoplight
|
|
|
32
32
|
#
|
|
33
33
|
# @return [String]
|
|
34
34
|
def state
|
|
35
|
-
|
|
36
|
-
.data_store
|
|
37
|
-
.get_metadata(config)
|
|
38
|
-
.locked_state
|
|
35
|
+
metadata.locked_state
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
# Returns current color:
|
|
@@ -49,10 +46,7 @@ module Stoplight
|
|
|
49
46
|
#
|
|
50
47
|
# @return [String] returns current light color
|
|
51
48
|
def color
|
|
52
|
-
|
|
53
|
-
.data_store
|
|
54
|
-
.get_metadata(config)
|
|
55
|
-
.color
|
|
49
|
+
metadata.color
|
|
56
50
|
end
|
|
57
51
|
|
|
58
52
|
# Runs the given block of code with this circuit breaker
|
|
@@ -72,8 +66,10 @@ module Stoplight
|
|
|
72
66
|
def run(fallback = nil, &code)
|
|
73
67
|
raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
metadata.then do |metadata|
|
|
70
|
+
strategy = state_strategy_factory(metadata.color)
|
|
71
|
+
strategy.execute(fallback, metadata:, &code)
|
|
72
|
+
end
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
|
@@ -186,5 +182,10 @@ module Stoplight
|
|
|
186
182
|
def reconfigure(config)
|
|
187
183
|
self.class.new(config)
|
|
188
184
|
end
|
|
185
|
+
|
|
186
|
+
# @return [Stoplight::Metadata]
|
|
187
|
+
def metadata
|
|
188
|
+
config.data_store.get_metadata(config)
|
|
189
|
+
end
|
|
189
190
|
end
|
|
190
191
|
end
|
data/lib/stoplight/metadata.rb
CHANGED
|
@@ -16,9 +16,11 @@ module Stoplight
|
|
|
16
16
|
:locked_state,
|
|
17
17
|
:recovery_scheduled_after,
|
|
18
18
|
:recovery_started_at,
|
|
19
|
-
:recovered_at
|
|
19
|
+
:recovered_at,
|
|
20
|
+
:current_time
|
|
20
21
|
) do
|
|
21
22
|
def initialize(
|
|
23
|
+
current_time: Time.now,
|
|
22
24
|
successes: 0,
|
|
23
25
|
errors: 0,
|
|
24
26
|
recovery_probe_successes: 0,
|
|
@@ -49,6 +51,7 @@ module Stoplight
|
|
|
49
51
|
recovery_scheduled_after: (Time.at(Integer(recovery_scheduled_after)) if recovery_scheduled_after),
|
|
50
52
|
recovery_started_at: (Time.at(Integer(recovery_started_at)) if recovery_started_at),
|
|
51
53
|
recovered_at: (Time.at(Integer(recovered_at)) if recovered_at),
|
|
54
|
+
current_time:,
|
|
52
55
|
)
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -59,17 +62,16 @@ module Stoplight
|
|
|
59
62
|
# @param kwargs [Hash{Symbol => Object}]
|
|
60
63
|
# @return [Metadata]
|
|
61
64
|
def with(**kwargs)
|
|
62
|
-
self.class.new(**to_h.merge(kwargs))
|
|
65
|
+
self.class.new(**to_h.merge(current_time: Time.now, **kwargs))
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
# @param at [Time] (Time.now) the moment of time when the color is determined
|
|
66
68
|
# @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
|
|
67
|
-
def color
|
|
69
|
+
def color
|
|
68
70
|
if locked_state == State::LOCKED_GREEN
|
|
69
71
|
Color::GREEN
|
|
70
72
|
elsif locked_state == State::LOCKED_RED
|
|
71
73
|
Color::RED
|
|
72
|
-
elsif (recovery_scheduled_after && recovery_scheduled_after <
|
|
74
|
+
elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
|
|
73
75
|
Color::YELLOW
|
|
74
76
|
elsif breached_at
|
|
75
77
|
Color::RED
|
|
@@ -14,13 +14,13 @@ module Stoplight
|
|
|
14
14
|
# reach the threshold.
|
|
15
15
|
#
|
|
16
16
|
# @example With window-based configuration
|
|
17
|
-
# traffic_control = Stoplight::
|
|
17
|
+
# traffic_control = Stoplight::TrafficControl::ConsecutiveErrors.new
|
|
18
18
|
# config = Stoplight::Light::Config.new(threshold: 5, window_size: 60, traffic_control:)
|
|
19
19
|
#
|
|
20
20
|
# Will switch to red if 5 consecutive failures occur within the 60-second window
|
|
21
21
|
#
|
|
22
22
|
# @example With total number of consecutive failures configuration
|
|
23
|
-
# traffic_control = Stoplight::
|
|
23
|
+
# traffic_control = Stoplight::TrafficControl::ConsecutiveErrors.new
|
|
24
24
|
# config = Stoplight::Light::Config.new(threshold: 5, window_size: nil, traffic_control:)
|
|
25
25
|
#
|
|
26
26
|
# Will switch to red only if 5 consecutive failures occur regardless of the time window
|
|
@@ -5,14 +5,14 @@ module Stoplight
|
|
|
5
5
|
# A strategy that stops the traffic based on error rate.
|
|
6
6
|
#
|
|
7
7
|
# @example
|
|
8
|
-
# traffic_control = Stoplight::
|
|
8
|
+
# traffic_control = Stoplight::TrafficControl::ErrorRate.new
|
|
9
9
|
# config = Stoplight::Light::Config.new(threshold: 0.6, window_size: 300, traffic_control:)
|
|
10
10
|
#
|
|
11
11
|
# Will switch to red if 60% error rate reached within the 5-minute (300 seconds) sliding window.
|
|
12
12
|
# By default this traffic control strategy starts evaluating only after 10 requests have been made. You can
|
|
13
13
|
# adjust this by passing a different value for `min_requests` when initializing the strategy.
|
|
14
14
|
#
|
|
15
|
-
# traffic_control = Stoplight::
|
|
15
|
+
# traffic_control = Stoplight::TrafficControl::ErrorRate.new(min_requests: 100)
|
|
16
16
|
#
|
|
17
17
|
# @api private
|
|
18
18
|
class ErrorRate < Base
|
data/lib/stoplight/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stoplight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cameron Desautels
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/stoplight/data_store/base.rb
|
|
68
68
|
- lib/stoplight/data_store/fail_safe.rb
|
|
69
69
|
- lib/stoplight/data_store/memory.rb
|
|
70
|
+
- lib/stoplight/data_store/memory/sliding_window.rb
|
|
70
71
|
- lib/stoplight/data_store/redis.rb
|
|
71
72
|
- lib/stoplight/data_store/redis/get_metadata.lua
|
|
72
73
|
- lib/stoplight/data_store/redis/lua.rb
|
|
@@ -76,7 +77,6 @@ files:
|
|
|
76
77
|
- lib/stoplight/data_store/redis/transition_to_red.lua
|
|
77
78
|
- lib/stoplight/data_store/redis/transition_to_yellow.lua
|
|
78
79
|
- lib/stoplight/default.rb
|
|
79
|
-
- lib/stoplight/empty_metadata.rb
|
|
80
80
|
- lib/stoplight/error.rb
|
|
81
81
|
- lib/stoplight/failure.rb
|
|
82
82
|
- lib/stoplight/light.rb
|