stoplight 5.4.0 → 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 +1 -1
- 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/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +22 -11
- 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 -43
- 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 -10
- data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
- data/lib/stoplight/data_store/memory.rb +0 -285
- data/lib/stoplight/data_store/redis/lua.rb +0 -23
- data/lib/stoplight/data_store/redis.rb +0 -446
- data/lib/stoplight/data_store.rb +0 -6
- data/lib/stoplight/default.rb +0 -30
- data/lib/stoplight/error.rb +0 -39
- data/lib/stoplight/failure.rb +0 -71
- data/lib/stoplight/light/config.rb +0 -112
- 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 -31
- 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,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
|
-
module Stoplight
|
|
6
|
-
class Light
|
|
7
|
-
# Implements light configuration behavior
|
|
8
|
-
module ConfigurationBuilderInterface
|
|
9
|
-
# Configures data store to be used with this circuit breaker
|
|
10
|
-
#
|
|
11
|
-
# @example
|
|
12
|
-
# Stoplight('example')
|
|
13
|
-
# .with_data_store(Stoplight::DataStore::Memory.new)
|
|
14
|
-
#
|
|
15
|
-
# @param data_store [DataStore::Base]
|
|
16
|
-
# @return [Stoplight::Light]
|
|
17
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
18
|
-
def with_data_store(data_store)
|
|
19
|
-
reconfigure(config.with(data_store: data_store))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Configures cool off time. Stoplight automatically tries to recover
|
|
23
|
-
# from the red state after the cool off time.
|
|
24
|
-
#
|
|
25
|
-
# @example
|
|
26
|
-
# Stoplight('example')
|
|
27
|
-
# .cool_off_time(60)
|
|
28
|
-
#
|
|
29
|
-
# @param cool_off_time [Numeric] number of seconds
|
|
30
|
-
# @return [Stoplight::Light]
|
|
31
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
32
|
-
def with_cool_off_time(cool_off_time)
|
|
33
|
-
reconfigure(config.with(cool_off_time: cool_off_time))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Configures custom threshold. After this number of failures Stoplight
|
|
37
|
-
# switches to the red state:
|
|
38
|
-
#
|
|
39
|
-
# @example
|
|
40
|
-
# Stoplight('example')
|
|
41
|
-
# .with_threshold(5)
|
|
42
|
-
#
|
|
43
|
-
# @param threshold [Numeric]
|
|
44
|
-
# @return [Stoplight::Light]
|
|
45
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
46
|
-
def with_threshold(threshold)
|
|
47
|
-
reconfigure(config.with(threshold: threshold))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Configures custom window size which Stoplight uses to count failures. For example,
|
|
51
|
-
#
|
|
52
|
-
# @example
|
|
53
|
-
# Stoplight('example')
|
|
54
|
-
# .with_threshold(5)
|
|
55
|
-
# .with_window_size(60)
|
|
56
|
-
#
|
|
57
|
-
# The above example will turn to red light only when 5 errors happen
|
|
58
|
-
# within 60 seconds period.
|
|
59
|
-
#
|
|
60
|
-
# @param window_size [Numeric] number of seconds
|
|
61
|
-
# @return [Stoplight::Light]
|
|
62
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
63
|
-
def with_window_size(window_size)
|
|
64
|
-
reconfigure(config.with(window_size: window_size))
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Configures custom notifier
|
|
68
|
-
#
|
|
69
|
-
# @example
|
|
70
|
-
# io = StringIO.new
|
|
71
|
-
# notifier = Stoplight::Notifier::IO.new(io)
|
|
72
|
-
# Stoplight('example')
|
|
73
|
-
# .with_notifiers([notifier])
|
|
74
|
-
#
|
|
75
|
-
# @param notifiers [Array<Notifier::Base>]
|
|
76
|
-
# @return [Stoplight::Light]
|
|
77
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
78
|
-
def with_notifiers(notifiers)
|
|
79
|
-
reconfigure(config.with(notifiers: notifiers))
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# @param error_notifier [Proc]
|
|
83
|
-
# @return [Stoplight::Light]
|
|
84
|
-
# @api private
|
|
85
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
86
|
-
def with_error_notifier(&error_notifier)
|
|
87
|
-
reconfigure(config.with(error_notifier: error_notifier))
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Configures a custom list of tracked errors that counts toward the threshold.
|
|
91
|
-
#
|
|
92
|
-
# @example
|
|
93
|
-
# light = Stoplight('example')
|
|
94
|
-
# .with_tracked_errors(TimeoutError, NetworkError)
|
|
95
|
-
# light.run { call_external_service }
|
|
96
|
-
#
|
|
97
|
-
# In the example above, the +TimeoutError+ and +NetworkError+ exceptions
|
|
98
|
-
# will be counted towards the threshold for moving the circuit breaker into the red state.
|
|
99
|
-
# If not configured, the default tracked error is +StandardError+.
|
|
100
|
-
#
|
|
101
|
-
# @param tracked_errors [Array<StandardError>]
|
|
102
|
-
# @return [Stoplight::Light]
|
|
103
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
104
|
-
def with_tracked_errors(*tracked_errors)
|
|
105
|
-
reconfigure(config.with(tracked_errors: tracked_errors.dup.freeze))
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Configures a custom list of skipped errors that do not count toward the threshold.
|
|
109
|
-
# Typically, such errors does not represent a real failure and handled somewhere else
|
|
110
|
-
# in the code.
|
|
111
|
-
#
|
|
112
|
-
# @example
|
|
113
|
-
# light = Stoplight('example')
|
|
114
|
-
# .with_skipped_errors(ActiveRecord::RecordNotFound)
|
|
115
|
-
# light.run { User.find(123) }
|
|
116
|
-
#
|
|
117
|
-
# In the example above, the +ActiveRecord::RecordNotFound+ doesn't
|
|
118
|
-
# move the circuit breaker into the red state.
|
|
119
|
-
#
|
|
120
|
-
# @param skipped_errors [Array<Exception>]
|
|
121
|
-
# @return [Stoplight::Light]
|
|
122
|
-
# @deprecated consider using +Light#with+ for reconfiguration
|
|
123
|
-
def with_skipped_errors(*skipped_errors)
|
|
124
|
-
reconfigure(config.with(skipped_errors: skipped_errors))
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
class Light
|
|
5
|
-
# Defines how the light executes when it is green.
|
|
6
|
-
#
|
|
7
|
-
# This strategy clears failures after successful execution and handles errors
|
|
8
|
-
# by either raising them or invoking a fallback if provided.
|
|
9
|
-
#
|
|
10
|
-
# @api private
|
|
11
|
-
class GreenRunStrategy < RunStrategy
|
|
12
|
-
# Executes the provided code block when the light is in the green state.
|
|
13
|
-
#
|
|
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.
|
|
16
|
-
# @yield The code block to execute.
|
|
17
|
-
# @return [Object] The result of the code block if successful.
|
|
18
|
-
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
19
|
-
def execute(fallback, metadata:, &code)
|
|
20
|
-
# TODO: Consider implementing sampling rate to limit the memory footprint
|
|
21
|
-
code.call.tap { record_success }
|
|
22
|
-
rescue => error
|
|
23
|
-
if config.track_error?(error)
|
|
24
|
-
record_error(error)
|
|
25
|
-
|
|
26
|
-
if fallback
|
|
27
|
-
fallback.call(error)
|
|
28
|
-
else
|
|
29
|
-
raise
|
|
30
|
-
end
|
|
31
|
-
else
|
|
32
|
-
# User chose to not track the error, so we record it as a success
|
|
33
|
-
record_success
|
|
34
|
-
raise
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private def record_error(error)
|
|
39
|
-
failure = Stoplight::Failure.from_error(error)
|
|
40
|
-
metadata = data_store.record_failure(config, failure)
|
|
41
|
-
|
|
42
|
-
if config.traffic_control.stop_traffic?(config, metadata) && data_store.transition_to_color(config, Color::RED)
|
|
43
|
-
config.notifiers.each do |notifier|
|
|
44
|
-
notifier.notify(config, Color::GREEN, Color::RED, error)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private def record_success
|
|
50
|
-
data_store.record_success(config)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
class Light
|
|
5
|
-
# Defines how the light executes when it is red.
|
|
6
|
-
#
|
|
7
|
-
# This strategy prevents execution of the code block and either raises an error
|
|
8
|
-
# or invokes a fallback if provided.
|
|
9
|
-
#
|
|
10
|
-
# @api private
|
|
11
|
-
class RedRunStrategy < RunStrategy
|
|
12
|
-
# Executes the fallback proc when the light is in the red state.
|
|
13
|
-
#
|
|
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.
|
|
16
|
-
# @return [Object, nil] The result of the fallback proc if provided.
|
|
17
|
-
# @raise [Stoplight::Error::RedLight] Raises an error if no fallback is provided.
|
|
18
|
-
def execute(fallback, metadata:)
|
|
19
|
-
if fallback
|
|
20
|
-
fallback.call(nil)
|
|
21
|
-
else
|
|
22
|
-
raise Error::RedLight.new(
|
|
23
|
-
config.name,
|
|
24
|
-
cool_off_time: config.cool_off_time,
|
|
25
|
-
retry_after: metadata.recovery_scheduled_after
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
class Light
|
|
5
|
-
# Represents an abstract strategy for running a light's operations.
|
|
6
|
-
# Every new strategy should be a child of this class.
|
|
7
|
-
#
|
|
8
|
-
# @api private
|
|
9
|
-
# @abstract
|
|
10
|
-
class RunStrategy
|
|
11
|
-
# @!attribute [r] config
|
|
12
|
-
# @return [Stoplight::Light::Config] The configuration for the light.
|
|
13
|
-
private attr_reader :config
|
|
14
|
-
|
|
15
|
-
# @!attribute [r] data_store
|
|
16
|
-
# @return [Stoplight::DataStore::Base] The data store associated with the light.
|
|
17
|
-
private attr_reader :data_store
|
|
18
|
-
|
|
19
|
-
# @param config [Stoplight::Light::Config] The configuration for the light.
|
|
20
|
-
def initialize(config)
|
|
21
|
-
@config = config
|
|
22
|
-
@data_store = config.data_store
|
|
23
|
-
end
|
|
24
|
-
|
|
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)
|
|
28
|
-
raise NotImplementedError, "Subclasses must implement the execute method"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
class Light
|
|
5
|
-
# Defines how the light executes when it is yellow.
|
|
6
|
-
#
|
|
7
|
-
# This strategy clears failures after successful execution and notifies
|
|
8
|
-
# about color switch from Red to Green. It also handles errors by either
|
|
9
|
-
# raising them or invoking a fallback if provided.
|
|
10
|
-
#
|
|
11
|
-
# @api private
|
|
12
|
-
class YellowRunStrategy < RunStrategy
|
|
13
|
-
# Executes the provided code block when the light is in the yellow state.
|
|
14
|
-
#
|
|
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.
|
|
17
|
-
# @yield The code block to execute.
|
|
18
|
-
# @return [Object] The result of the code block if successful.
|
|
19
|
-
# @raise [Exception] Re-raises the error if it is not tracked or no fallback is provided.
|
|
20
|
-
def execute(fallback, metadata:, &code)
|
|
21
|
-
transition_to_yellow(metadata:)
|
|
22
|
-
# TODO: We need to employ a probabilistic approach here to avoid "thundering herd" problem
|
|
23
|
-
code.call.tap { record_recovery_probe_success }
|
|
24
|
-
rescue => error
|
|
25
|
-
if config.track_error?(error)
|
|
26
|
-
record_recovery_probe_failure(error)
|
|
27
|
-
|
|
28
|
-
if fallback
|
|
29
|
-
fallback.call(error)
|
|
30
|
-
else
|
|
31
|
-
raise
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
record_recovery_probe_success
|
|
35
|
-
raise
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private def record_recovery_probe_success
|
|
40
|
-
metadata = data_store.record_recovery_probe_success(config)
|
|
41
|
-
|
|
42
|
-
recover(metadata)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private def record_recovery_probe_failure(error)
|
|
46
|
-
failure = Failure.from_error(error)
|
|
47
|
-
metadata = data_store.record_recovery_probe_failure(config, failure)
|
|
48
|
-
|
|
49
|
-
recover(metadata)
|
|
50
|
-
end
|
|
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
|
-
|
|
64
|
-
private def recover(metadata)
|
|
65
|
-
recovery_result = config.traffic_recovery.determine_color(config, metadata)
|
|
66
|
-
|
|
67
|
-
case recovery_result
|
|
68
|
-
when TrafficRecovery::GREEN
|
|
69
|
-
if data_store.transition_to_color(config, Color::GREEN)
|
|
70
|
-
config.notifiers.each do |notifier|
|
|
71
|
-
notifier.notify(config, Color::YELLOW, Color::GREEN, nil)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
when TrafficRecovery::YELLOW
|
|
75
|
-
if data_store.transition_to_color(config, Color::YELLOW)
|
|
76
|
-
config.notifiers.each do |notifier|
|
|
77
|
-
notifier.notify(config, Color::RED, Color::YELLOW, nil)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
when TrafficRecovery::RED
|
|
81
|
-
if data_store.transition_to_color(config, Color::RED)
|
|
82
|
-
config.notifiers.each do |notifier|
|
|
83
|
-
notifier.notify(config, Color::YELLOW, Color::RED, nil)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
when TrafficRecovery::PASS
|
|
87
|
-
# No state change, do nothing
|
|
88
|
-
else
|
|
89
|
-
raise "recovery strategy returned an expected color: #{recovery_result}"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
data/lib/stoplight/light.rb
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
#
|
|
5
|
-
# @api private use +Stoplight()+ method instead
|
|
6
|
-
class Light
|
|
7
|
-
extend Forwardable
|
|
8
|
-
include ConfigurationBuilderInterface
|
|
9
|
-
|
|
10
|
-
# @!attribute [r] config
|
|
11
|
-
# @return [Stoplight::Light::Config]
|
|
12
|
-
# @api private
|
|
13
|
-
attr_reader :config
|
|
14
|
-
|
|
15
|
-
# @!attribute [r] name
|
|
16
|
-
# The name of the light.
|
|
17
|
-
# @return [String]
|
|
18
|
-
def_delegator :config, :name
|
|
19
|
-
|
|
20
|
-
# @param config [Stoplight::Light::Config]
|
|
21
|
-
def initialize(config, green_run_strategy: nil, yellow_run_strategy: nil, red_run_strategy: nil)
|
|
22
|
-
@config = config
|
|
23
|
-
@green_run_strategy = green_run_strategy
|
|
24
|
-
@yellow_run_strategy = yellow_run_strategy
|
|
25
|
-
@red_run_strategy = red_run_strategy
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Returns the current state of the light:
|
|
29
|
-
# * +Stoplight::State::LOCKED_GREEN+ -- light is locked green and allows all traffic
|
|
30
|
-
# * +Stoplight::State::LOCKED_RED+ -- light is locked red and blocks all traffic
|
|
31
|
-
# * +Stoplight::State::UNLOCKED+ -- light is not locked and follow the configured rules
|
|
32
|
-
#
|
|
33
|
-
# @return [String]
|
|
34
|
-
def state
|
|
35
|
-
metadata.locked_state
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Returns current color:
|
|
39
|
-
# * +Stoplight::Color::GREEN+ -- circuit breaker is closed
|
|
40
|
-
# * +Stoplight::Color::RED+ -- circuit breaker is open
|
|
41
|
-
# * +Stoplight::Color::YELLOW+ -- circuit breaker is half-open
|
|
42
|
-
#
|
|
43
|
-
# @example
|
|
44
|
-
# light = Stoplight('example')
|
|
45
|
-
# light.color #=> Color::GREEN
|
|
46
|
-
#
|
|
47
|
-
# @return [String] returns current light color
|
|
48
|
-
def color
|
|
49
|
-
metadata.color
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Runs the given block of code with this circuit breaker
|
|
53
|
-
#
|
|
54
|
-
# @example
|
|
55
|
-
# light = Stoplight('example')
|
|
56
|
-
# light.run { 2/0 }
|
|
57
|
-
#
|
|
58
|
-
# @example Running with fallback
|
|
59
|
-
# light = Stoplight('example')
|
|
60
|
-
# light.run(->(error) { 0 }) { 1 / 0 } #=> 0
|
|
61
|
-
#
|
|
62
|
-
# @param fallback [Proc, nil] (nil) fallback code to run if the circuit breaker is open
|
|
63
|
-
# @raise [Stoplight::Error::RedLight]
|
|
64
|
-
# @return [any]
|
|
65
|
-
# @raise [Error::RedLight]
|
|
66
|
-
def run(fallback = nil, &code)
|
|
67
|
-
raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
|
|
68
|
-
|
|
69
|
-
metadata.then do |metadata|
|
|
70
|
-
strategy = state_strategy_factory(metadata.color)
|
|
71
|
-
strategy.execute(fallback, metadata:, &code)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
|
76
|
-
#
|
|
77
|
-
# @example
|
|
78
|
-
# light = Stoplight('example-locked')
|
|
79
|
-
# light.lock(Stoplight::Color::RED)
|
|
80
|
-
#
|
|
81
|
-
# @param color [String] should be either +Color::RED+ or +Color::GREEN+
|
|
82
|
-
# @return [Stoplight::Light] returns locked light (circuit breaker)
|
|
83
|
-
def lock(color)
|
|
84
|
-
state = case color
|
|
85
|
-
when Color::RED then State::LOCKED_RED
|
|
86
|
-
when Color::GREEN then State::LOCKED_GREEN
|
|
87
|
-
else raise Error::IncorrectColor
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
config.data_store.set_state(config, state)
|
|
91
|
-
|
|
92
|
-
self
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Unlocks light and sets its state to State::UNLOCKED
|
|
96
|
-
#
|
|
97
|
-
# @example
|
|
98
|
-
# light = Stoplight('example-locked')
|
|
99
|
-
# light.lock(Stoplight::Color::RED)
|
|
100
|
-
# light.unlock
|
|
101
|
-
#
|
|
102
|
-
# @return [Stoplight::Light] returns unlocked light (circuit breaker)
|
|
103
|
-
def unlock
|
|
104
|
-
config.data_store.set_state(config, Stoplight::State::UNLOCKED)
|
|
105
|
-
|
|
106
|
-
self
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Two lights considered equal if they have the same configuration.
|
|
110
|
-
#
|
|
111
|
-
# @param other [any]
|
|
112
|
-
# @return [Boolean]
|
|
113
|
-
def ==(other)
|
|
114
|
-
other.is_a?(self.class) && config == other.config
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Reconfigures the light with updated settings and returns a new instance.
|
|
118
|
-
#
|
|
119
|
-
# This method allows you to modify the configuration of a +Stoplight::Light+ object
|
|
120
|
-
# by providing a hash of settings. The original light remains unchanged, and a new
|
|
121
|
-
# light instance with the updated configuration is returned.
|
|
122
|
-
#
|
|
123
|
-
# @param settings [Hash] A hash of configuration options to update.
|
|
124
|
-
# @option settings [String] :name The name of the light.
|
|
125
|
-
# @option settings [Numeric] :cool_off_time The cool-off time in seconds before the light attempts recovery.
|
|
126
|
-
# @option settings [Numeric] :threshold The failure threshold to trigger the red state.
|
|
127
|
-
# @option settings [Numeric] :window_size The time window in seconds for counting failures.
|
|
128
|
-
# @option settings [Stoplight::DataStore::Base] :data_store The data store to use for persisting light state.
|
|
129
|
-
# @option settings [Array<Stoplight::Notifier::Base>] :notifiers A list of notifiers to handle light events.
|
|
130
|
-
# @option settings [Proc] :error_notifier A custom error notifier to handle exceptions.
|
|
131
|
-
# @option settings [Array<StandardError>] :tracked_errors A list of errors to track for failure counting.
|
|
132
|
-
# @option settings [Array<StandardError>] :skipped_errors A list of errors to skip from failure counting.
|
|
133
|
-
# @return [Stoplight::Light] A new `Stoplight::Light` instance with the updated configuration.
|
|
134
|
-
#
|
|
135
|
-
# @example Reconfiguring a light with custom settings
|
|
136
|
-
# light = Stoplight('payment-api')
|
|
137
|
-
#
|
|
138
|
-
# # Create a light for invoices with a higher threshold
|
|
139
|
-
# invoices_light = light.with(tracked_errors: [TimeoutError], threshold: 10)
|
|
140
|
-
#
|
|
141
|
-
# # Create a light for payments with a lower threshold
|
|
142
|
-
# payment_light = light.with(threshold: 5)
|
|
143
|
-
#
|
|
144
|
-
# # Run the lights with their respective configurations
|
|
145
|
-
# invoices_light.run(->(error) { [] }) { call_invoices_api }
|
|
146
|
-
# payment_light.run(->(error) { nil }) { call_payment_api }
|
|
147
|
-
# @see +Stoplight()+
|
|
148
|
-
def with(**settings)
|
|
149
|
-
reconfigure(config.with(**settings))
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
private
|
|
153
|
-
|
|
154
|
-
def state_strategy_factory(color)
|
|
155
|
-
case color
|
|
156
|
-
when Color::GREEN
|
|
157
|
-
green_run_strategy
|
|
158
|
-
when Color::YELLOW
|
|
159
|
-
yellow_run_strategy
|
|
160
|
-
else
|
|
161
|
-
red_run_strategy
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# @return [Stoplight::Runnable::RunStrategy]
|
|
166
|
-
def green_run_strategy
|
|
167
|
-
@green_run_strategy ||= GreenRunStrategy.new(config)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# @return [Stoplight::Runnable::RunStrategy]
|
|
171
|
-
def yellow_run_strategy
|
|
172
|
-
@yellow_run_strategy ||= YellowRunStrategy.new(config)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# @return [Stoplight::Runnable::RunStrategy]
|
|
176
|
-
def red_run_strategy
|
|
177
|
-
@red_run_strategy ||= RedRunStrategy.new(config)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# @param config [Stoplight::Light::Config]
|
|
181
|
-
# @return [Stoplight::Light]
|
|
182
|
-
def reconfigure(config)
|
|
183
|
-
self.class.new(config)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# @return [Stoplight::Metadata]
|
|
187
|
-
def metadata
|
|
188
|
-
config.data_store.get_metadata(config)
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
data/lib/stoplight/metadata.rb
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Stoplight
|
|
4
|
-
# @api private
|
|
5
|
-
Metadata = Data.define(
|
|
6
|
-
:successes,
|
|
7
|
-
:errors,
|
|
8
|
-
:recovery_probe_successes,
|
|
9
|
-
:recovery_probe_errors,
|
|
10
|
-
:last_error_at,
|
|
11
|
-
:last_success_at,
|
|
12
|
-
:consecutive_errors,
|
|
13
|
-
:consecutive_successes,
|
|
14
|
-
:last_error,
|
|
15
|
-
:breached_at,
|
|
16
|
-
:locked_state,
|
|
17
|
-
:recovery_scheduled_after,
|
|
18
|
-
:recovery_started_at,
|
|
19
|
-
:recovered_at,
|
|
20
|
-
:current_time
|
|
21
|
-
) do
|
|
22
|
-
def initialize(
|
|
23
|
-
current_time: Time.now,
|
|
24
|
-
successes: 0,
|
|
25
|
-
errors: 0,
|
|
26
|
-
recovery_probe_successes: 0,
|
|
27
|
-
recovery_probe_errors: 0,
|
|
28
|
-
last_error_at: nil,
|
|
29
|
-
last_success_at: nil,
|
|
30
|
-
consecutive_errors: 0,
|
|
31
|
-
consecutive_successes: 0,
|
|
32
|
-
last_error: nil,
|
|
33
|
-
breached_at: nil,
|
|
34
|
-
locked_state: nil,
|
|
35
|
-
recovery_started_at: nil,
|
|
36
|
-
recovery_scheduled_after: nil,
|
|
37
|
-
recovered_at: nil
|
|
38
|
-
)
|
|
39
|
-
super(
|
|
40
|
-
recovery_probe_successes: recovery_probe_successes.to_i,
|
|
41
|
-
recovery_probe_errors: recovery_probe_errors.to_i,
|
|
42
|
-
successes: successes.to_i,
|
|
43
|
-
errors: errors.to_i,
|
|
44
|
-
last_error_at: (Time.at(Integer(last_error_at)) if last_error_at),
|
|
45
|
-
last_success_at: (Time.at(Integer(last_success_at)) if last_success_at),
|
|
46
|
-
consecutive_errors: consecutive_errors.to_i,
|
|
47
|
-
consecutive_successes: consecutive_successes.to_i,
|
|
48
|
-
last_error:,
|
|
49
|
-
breached_at: (Time.at(Integer(breached_at)) if breached_at),
|
|
50
|
-
locked_state: locked_state || State::UNLOCKED,
|
|
51
|
-
recovery_scheduled_after: (Time.at(Integer(recovery_scheduled_after)) if recovery_scheduled_after),
|
|
52
|
-
recovery_started_at: (Time.at(Integer(recovery_started_at)) if recovery_started_at),
|
|
53
|
-
recovered_at: (Time.at(Integer(recovered_at)) if recovered_at),
|
|
54
|
-
current_time:,
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Creates a new Metadata instance with updated attributes. This method overrides
|
|
59
|
-
# the default +with+ method provided by +Data.define+ to ensure constructor
|
|
60
|
-
# logic is applied.
|
|
61
|
-
#
|
|
62
|
-
# @param kwargs [Hash{Symbol => Object}]
|
|
63
|
-
# @return [Metadata]
|
|
64
|
-
def with(**kwargs)
|
|
65
|
-
self.class.new(**to_h.merge(current_time: Time.now, **kwargs))
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# @return [String] one of +Color::GREEN+, +Color::RED+, or +Color::YELLOW+
|
|
69
|
-
def color
|
|
70
|
-
if locked_state == State::LOCKED_GREEN
|
|
71
|
-
Color::GREEN
|
|
72
|
-
elsif locked_state == State::LOCKED_RED
|
|
73
|
-
Color::RED
|
|
74
|
-
elsif (recovery_scheduled_after && recovery_scheduled_after < current_time) || recovery_started_at
|
|
75
|
-
Color::YELLOW
|
|
76
|
-
elsif breached_at
|
|
77
|
-
Color::RED
|
|
78
|
-
else
|
|
79
|
-
Color::GREEN
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Calculates the error rate based on the number of successes and errors.
|
|
84
|
-
#
|
|
85
|
-
# @return [Float]
|
|
86
|
-
def error_rate
|
|
87
|
-
if (successes + errors).zero?
|
|
88
|
-
0.0
|
|
89
|
-
else
|
|
90
|
-
errors.fdiv(successes + errors)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# @return [Integer]
|
|
95
|
-
def requests
|
|
96
|
-
successes + errors
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|