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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 297bbf636a9d6fa8e47b2241a3bfe0ccf357954044dc9cd02b79735012d92148
|
|
4
|
+
data.tar.gz: 7e539fac482dbde352b0e971b20d29968758c85017c2f917859d5130ef8ed7a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53277c7ec692204dc99c96dbf4d8e224d47424b6f5605445de2dc9122d09adedcb683ca783be7f0496db0b6e9aef9045636100da889055e0cb0ee4a4c398ab1e
|
|
7
|
+
data.tar.gz: f0dd133ab0aa988a9bac5d9b2954299c5cf47ee86f08482b0199c8108fe953ae6e44c5f781457cda74063a93d1686da9ce76b4e4780ffb16d66dd70790b17815
|
data/README.md
CHANGED
|
@@ -628,7 +628,7 @@ Example: "Ruby 3.2 reaches end-of-life in March 2026, so Stoplight 6.0 will requ
|
|
|
628
628
|
|
|
629
629
|
After checking out the repo, run `bundle install` to install dependencies. Run tests with `bundle exec rspec` and check
|
|
630
630
|
code style with `bundle exec standardrb`. We follow a git flow branching strategy - see our [Git Flow wiki page] for
|
|
631
|
-
details on branch naming, releases, and contribution workflow.
|
|
631
|
+
details on branch naming, releases, and contribution workflow. Also check our CONTRIBUTING.md guide for contributors.
|
|
632
632
|
|
|
633
633
|
## Credits
|
|
634
634
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<title>Stoplight Admin</title>
|
|
8
8
|
|
|
9
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@latest/dist/turbo.es2017-esm.min.js"></script>
|
|
10
|
-
<script type="module">
|
|
9
|
+
<script nonce="<%= nonce %>" type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@latest/dist/turbo.es2017-esm.min.js"></script>
|
|
10
|
+
<script nonce="<%= nonce %>" type="module">
|
|
11
11
|
document.addEventListener("turbo:load", () => {
|
|
12
12
|
window.initFlowbite()
|
|
13
13
|
})
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
</footer>
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
|
-
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
|
66
|
+
<script nonce="<%= nonce %>" src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
|
67
67
|
</body>
|
|
68
68
|
</html>
|
data/lib/stoplight/admin.rb
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
4
|
require "sinatra/base"
|
|
5
|
-
require "sinatra/contrib"
|
|
6
5
|
require "sinatra/json"
|
|
7
6
|
rescue LoadError
|
|
8
7
|
raise <<~WARN
|
|
9
|
-
"sinatra" and "sinatra-contrib" gems are unavailable and
|
|
8
|
+
"sinatra" and "sinatra-contrib" gems are unavailable and necessary for running Stoplight Admin panel
|
|
10
9
|
Please add them to your Gemfile and run `bundle install`:
|
|
11
10
|
gem "sinatra", required: false
|
|
12
11
|
gem "sinatra-contrib", require: false
|
|
@@ -25,13 +24,14 @@ module Stoplight
|
|
|
25
24
|
helpers Helpers
|
|
26
25
|
|
|
27
26
|
set :protection, except: %i[json_csrf]
|
|
28
|
-
set :data_store, proc { Stoplight.
|
|
27
|
+
set :data_store, proc { Stoplight.__stoplight__default_configuration.data_store }
|
|
29
28
|
set :views, File.join(__dir__, "admin", "views")
|
|
29
|
+
set :nonce, proc { |request| }
|
|
30
30
|
|
|
31
31
|
get "/" do
|
|
32
32
|
lights, stats = dependencies.stats_action.call
|
|
33
33
|
|
|
34
|
-
erb :index, locals: stats.merge(lights: lights)
|
|
34
|
+
erb :index, locals: stats.merge(lights: lights, nonce: settings.nonce(request))
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
get "/stats" do
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stoplight
|
|
4
|
-
module
|
|
4
|
+
module Domain
|
|
5
5
|
# The +CompatibilityResult+ class represents the result of a compatibility check
|
|
6
6
|
# for a strategy. It provides methods to determine if the strategy is compatible
|
|
7
7
|
# and to retrieve error messages when it is not.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
# A +Stoplight::Light+ configuration object.
|
|
6
|
+
#
|
|
7
|
+
# # @!attribute [r] name
|
|
8
|
+
# @return [String]
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [r] cool_off_time
|
|
11
|
+
# @return [Numeric]
|
|
12
|
+
#
|
|
13
|
+
# @!attribute [r] threshold
|
|
14
|
+
# @return [Numeric]
|
|
15
|
+
#
|
|
16
|
+
# @!attribute [r] window_size
|
|
17
|
+
# @return [Numeric]
|
|
18
|
+
#
|
|
19
|
+
# @!attribute [r] tracked_errors
|
|
20
|
+
# @return [Array<StandardError>]
|
|
21
|
+
#
|
|
22
|
+
# @!attribute [r] skipped_errors
|
|
23
|
+
# @return [Array<Exception>]
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
Config = Data.define(
|
|
27
|
+
:name,
|
|
28
|
+
:cool_off_time,
|
|
29
|
+
:threshold,
|
|
30
|
+
:recovery_threshold,
|
|
31
|
+
:window_size,
|
|
32
|
+
:tracked_errors,
|
|
33
|
+
:skipped_errors
|
|
34
|
+
) do
|
|
35
|
+
class << self
|
|
36
|
+
# Creates a new NULL configuration object.
|
|
37
|
+
# @return [Stoplight::Domain::Config]
|
|
38
|
+
def empty
|
|
39
|
+
new(**members.map { |key| [key, nil] }.to_h)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Checks if the given error should be tracked
|
|
44
|
+
#
|
|
45
|
+
# @param error [#==] The error to check, e.g. an Exception, Class or Proc
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def track_error?(error)
|
|
48
|
+
skip = skipped_errors.any? { |klass| klass === error }
|
|
49
|
+
track = tracked_errors.any? { |klass| klass === error }
|
|
50
|
+
|
|
51
|
+
!skip && track
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Stoplight
|
|
4
|
-
module
|
|
4
|
+
module Domain
|
|
5
5
|
# @abstract
|
|
6
|
-
|
|
6
|
+
# :nocov:
|
|
7
|
+
class DataStore
|
|
7
8
|
METRICS_RETENTION_TIME = 60 * 60 * 24 # 1 day
|
|
8
9
|
|
|
9
10
|
# Retrieves the names of all lights stored in the data store.
|
|
@@ -15,24 +16,24 @@ module Stoplight
|
|
|
15
16
|
|
|
16
17
|
# Retrieves metadata for a specific light configuration.
|
|
17
18
|
#
|
|
18
|
-
# @param config [Stoplight::
|
|
19
|
-
# @return [Stoplight::Metadata] The metadata associated with the light.
|
|
19
|
+
# @param config [Stoplight::Domain::Config] The light configuration.
|
|
20
|
+
# @return [Stoplight::Domain::Metadata] The metadata associated with the light.
|
|
20
21
|
def get_metadata(config)
|
|
21
22
|
raise NotImplementedError
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# Records a failure for a specific light configuration.
|
|
25
26
|
#
|
|
26
|
-
# @param config [Stoplight::
|
|
27
|
-
# @param
|
|
28
|
-
# @return [Stoplight::Metadata] The metadata associated with the light.
|
|
29
|
-
def record_failure(config,
|
|
27
|
+
# @param config [Stoplight::Domain::Config]
|
|
28
|
+
# @param exception [Exception]
|
|
29
|
+
# @return [Stoplight::Domain::Metadata] The metadata associated with the light.
|
|
30
|
+
def record_failure(config, exception)
|
|
30
31
|
raise NotImplementedError
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# Records a success for a specific light configuration.
|
|
34
35
|
#
|
|
35
|
-
# @param config [Stoplight::
|
|
36
|
+
# @param config [Stoplight::Domain::Config]
|
|
36
37
|
# @return [void]
|
|
37
38
|
def record_success(config)
|
|
38
39
|
raise NotImplementedError
|
|
@@ -40,24 +41,24 @@ module Stoplight
|
|
|
40
41
|
|
|
41
42
|
# Records a failed recovery probe for a specific light configuration.
|
|
42
43
|
#
|
|
43
|
-
# @param config [Stoplight::
|
|
44
|
+
# @param config [Stoplight::Domain::Config]
|
|
44
45
|
# @param failure [Failure]
|
|
45
|
-
# @return [Stoplight::Metadata]
|
|
46
|
+
# @return [Stoplight::Domain::Metadata]
|
|
46
47
|
def record_recovery_probe_failure(config, failure)
|
|
47
48
|
raise NotImplementedError
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# Records a successful recovery probe for a specific light configuration.
|
|
51
52
|
#
|
|
52
|
-
# @param config [Stoplight::
|
|
53
|
-
# @return [Stoplight::Metadata]
|
|
53
|
+
# @param config [Stoplight::Domain::Config]
|
|
54
|
+
# @return [Stoplight::Domain::Metadata]
|
|
54
55
|
def record_recovery_probe_success(config)
|
|
55
56
|
raise NotImplementedError
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# Sets the state of a specific light configuration.
|
|
59
60
|
#
|
|
60
|
-
# @param config [Stoplight::
|
|
61
|
+
# @param config [Stoplight::Domain::Config]
|
|
61
62
|
# @param state [String] The new state to set.
|
|
62
63
|
# @return [String] The state that was set.
|
|
63
64
|
def set_state(config, state)
|
|
@@ -71,7 +72,7 @@ module Stoplight
|
|
|
71
72
|
# is considered the "first" to perform the transition (and therefore responsible for
|
|
72
73
|
# triggering notifications).
|
|
73
74
|
#
|
|
74
|
-
# @param config [Stoplight::
|
|
75
|
+
# @param config [Stoplight::Domain::Config]
|
|
75
76
|
# @param color [String] The target color/state to transition to.
|
|
76
77
|
# Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
|
|
77
78
|
#
|
|
@@ -88,5 +89,6 @@ module Stoplight
|
|
|
88
89
|
raise NotImplementedError
|
|
89
90
|
end
|
|
90
91
|
end
|
|
92
|
+
# :nocov:
|
|
91
93
|
end
|
|
92
94
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
module Error
|
|
6
|
+
Base = Class.new(StandardError)
|
|
7
|
+
ConfigurationError = Class.new(Base)
|
|
8
|
+
IncorrectColor = Class.new(Base)
|
|
9
|
+
|
|
10
|
+
class RedLight < Base
|
|
11
|
+
# @!attribute light_name
|
|
12
|
+
# @return [String] The light's name
|
|
13
|
+
attr_reader :light_name
|
|
14
|
+
|
|
15
|
+
# @!attribute cool_off_time
|
|
16
|
+
# @return [Numeric] Cool-off period in seconds
|
|
17
|
+
attr_reader :cool_off_time
|
|
18
|
+
|
|
19
|
+
# @!attribute retry_after
|
|
20
|
+
# @return [Time] Absolute Time after which a recovery attempt can occur
|
|
21
|
+
attr_reader :retry_after
|
|
22
|
+
|
|
23
|
+
# Initializes a new RedLight error.
|
|
24
|
+
#
|
|
25
|
+
# @param light_name [String] The light's name
|
|
26
|
+
#
|
|
27
|
+
# @option cool_off_time [Numeric] Cool-off period in seconds
|
|
28
|
+
#
|
|
29
|
+
# @option retry_after [Time] Absolute Time after which a recovery attempt can occur
|
|
30
|
+
#
|
|
31
|
+
# @return [Stoplight::Error::RedLight]
|
|
32
|
+
def initialize(light_name, cool_off_time:, retry_after:)
|
|
33
|
+
@light_name = light_name
|
|
34
|
+
@cool_off_time = cool_off_time
|
|
35
|
+
@retry_after = retry_after
|
|
36
|
+
|
|
37
|
+
super(light_name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Stoplight
|
|
7
|
+
module Domain
|
|
8
|
+
# @api private
|
|
9
|
+
class Failure
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :error_class
|
|
12
|
+
# @return [String]
|
|
13
|
+
attr_reader :error_message
|
|
14
|
+
# @return [Time]
|
|
15
|
+
attr_reader :time
|
|
16
|
+
|
|
17
|
+
# @param error [Exception]
|
|
18
|
+
# @return (see #initialize)
|
|
19
|
+
def self.from_error(error, time: Time.now)
|
|
20
|
+
new(error.class.name, error.message, time)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param error_class [String]
|
|
24
|
+
# @param error_message [String]
|
|
25
|
+
# @param time [Time]
|
|
26
|
+
def initialize(error_class, error_message, time)
|
|
27
|
+
@error_class = error_class
|
|
28
|
+
@error_message = error_message
|
|
29
|
+
@time = time
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param other [Failure]
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def ==(other)
|
|
35
|
+
other.is_a?(self.class) &&
|
|
36
|
+
error_class == other.error_class &&
|
|
37
|
+
error_message == other.error_message &&
|
|
38
|
+
time == other.time
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Domain
|
|
7
|
+
class Light
|
|
8
|
+
# Implements light configuration behavior
|
|
9
|
+
module ConfigurationBuilderInterface
|
|
10
|
+
# Configures data store to be used with this circuit breaker
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# Stoplight('example')
|
|
14
|
+
# .with_data_store(Stoplight::DataStore::Memory.new)
|
|
15
|
+
#
|
|
16
|
+
# @param data_store [DataStore::Base]
|
|
17
|
+
# @return [Stoplight::Light]
|
|
18
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
19
|
+
def with_data_store(data_store)
|
|
20
|
+
with(data_store:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Configures cool off time. Stoplight automatically tries to recover
|
|
24
|
+
# from the red state after the cool off time.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# Stoplight('example')
|
|
28
|
+
# .cool_off_time(60)
|
|
29
|
+
#
|
|
30
|
+
# @param cool_off_time [Numeric] number of seconds
|
|
31
|
+
# @return [Stoplight::Light]
|
|
32
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
33
|
+
def with_cool_off_time(cool_off_time)
|
|
34
|
+
with(cool_off_time:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Configures custom threshold. After this number of failures Stoplight
|
|
38
|
+
# switches to the red state:
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# Stoplight('example')
|
|
42
|
+
# .with_threshold(5)
|
|
43
|
+
#
|
|
44
|
+
# @param threshold [Numeric]
|
|
45
|
+
# @return [Stoplight::Light]
|
|
46
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
47
|
+
def with_threshold(threshold)
|
|
48
|
+
with(threshold:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Configures custom window size which Stoplight uses to count failures. For example,
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# Stoplight('example')
|
|
55
|
+
# .with_threshold(5)
|
|
56
|
+
# .with_window_size(60)
|
|
57
|
+
#
|
|
58
|
+
# The above example will turn to red light only when 5 errors happen
|
|
59
|
+
# within 60 seconds period.
|
|
60
|
+
#
|
|
61
|
+
# @param window_size [Numeric] number of seconds
|
|
62
|
+
# @return [Stoplight::Light]
|
|
63
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
64
|
+
def with_window_size(window_size)
|
|
65
|
+
with(window_size:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Configures custom notifier
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# io = StringIO.new
|
|
72
|
+
# notifier = Stoplight::Notifier::IO.new(io)
|
|
73
|
+
# Stoplight('example')
|
|
74
|
+
# .with_notifiers([notifier])
|
|
75
|
+
#
|
|
76
|
+
# @param notifiers [Array<Notifier::Base>]
|
|
77
|
+
# @return [Stoplight::Light]
|
|
78
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
79
|
+
def with_notifiers(notifiers)
|
|
80
|
+
with(notifiers:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param error_notifier [Proc]
|
|
84
|
+
# @return [Stoplight::Light]
|
|
85
|
+
# @api private
|
|
86
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
87
|
+
def with_error_notifier(&error_notifier)
|
|
88
|
+
with(error_notifier: error_notifier)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Configures a custom list of tracked errors that counts toward the threshold.
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# light = Stoplight('example')
|
|
95
|
+
# .with_tracked_errors(TimeoutError, NetworkError)
|
|
96
|
+
# light.run { call_external_service }
|
|
97
|
+
#
|
|
98
|
+
# In the example above, the +TimeoutError+ and +NetworkError+ exceptions
|
|
99
|
+
# will be counted towards the threshold for moving the circuit breaker into the red state.
|
|
100
|
+
# If not configured, the default tracked error is +StandardError+.
|
|
101
|
+
#
|
|
102
|
+
# @param tracked_errors [Array<StandardError>]
|
|
103
|
+
# @return [Stoplight::Light]
|
|
104
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
105
|
+
def with_tracked_errors(*tracked_errors)
|
|
106
|
+
with(tracked_errors:)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Configures a custom list of skipped errors that do not count toward the threshold.
|
|
110
|
+
# Typically, such errors does not represent a real failure and handled somewhere else
|
|
111
|
+
# in the code.
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# light = Stoplight('example')
|
|
115
|
+
# .with_skipped_errors(ActiveRecord::RecordNotFound)
|
|
116
|
+
# light.run { User.find(123) }
|
|
117
|
+
#
|
|
118
|
+
# In the example above, the +ActiveRecord::RecordNotFound+ doesn't
|
|
119
|
+
# move the circuit breaker into the red state.
|
|
120
|
+
#
|
|
121
|
+
# @param skipped_errors [Array<Exception>]
|
|
122
|
+
# @return [Stoplight::Light]
|
|
123
|
+
# @deprecated consider using +Light#with+ for reconfiguration
|
|
124
|
+
def with_skipped_errors(*skipped_errors)
|
|
125
|
+
with(skipped_errors:)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Stoplight
|
|
6
|
+
module Domain
|
|
7
|
+
#
|
|
8
|
+
# @api private use +Stoplight()+ method instead
|
|
9
|
+
class Light
|
|
10
|
+
extend Forwardable
|
|
11
|
+
include ConfigurationBuilderInterface
|
|
12
|
+
|
|
13
|
+
# @!attribute [r] config
|
|
14
|
+
# @return [Stoplight::Domain::Config]
|
|
15
|
+
# @api private
|
|
16
|
+
attr_reader :config
|
|
17
|
+
|
|
18
|
+
# @!attribute [r] name
|
|
19
|
+
# The name of the light.
|
|
20
|
+
# @return [String]
|
|
21
|
+
def_delegator :config, :name
|
|
22
|
+
|
|
23
|
+
# @!attribute [r] green_run_strategy
|
|
24
|
+
# @return [Stoplight::Domain::Strategies::GreenRunStrategy]
|
|
25
|
+
protected attr_reader :green_run_strategy
|
|
26
|
+
|
|
27
|
+
# @!attribute [r] yellow_run_strategy
|
|
28
|
+
# @return [Stoplight::Domain::Strategies::YellowRunStrategy]
|
|
29
|
+
protected attr_reader :yellow_run_strategy
|
|
30
|
+
|
|
31
|
+
# @!attribute [r] red_run_strategy
|
|
32
|
+
# @return [Stoplight::Domain::Strategies::RedRunStrategy]
|
|
33
|
+
protected attr_reader :red_run_strategy
|
|
34
|
+
|
|
35
|
+
# @!attribute [r] data_store
|
|
36
|
+
# @return [Stoplight::Light::Base]
|
|
37
|
+
protected attr_reader :data_store
|
|
38
|
+
|
|
39
|
+
# @!attribute [r] factory
|
|
40
|
+
# @return [Stoplight::Domain::LightFactory]
|
|
41
|
+
protected attr_reader :factory
|
|
42
|
+
|
|
43
|
+
# @param config [Stoplight::Domain::Config]
|
|
44
|
+
def initialize(config, green_run_strategy:, yellow_run_strategy:, red_run_strategy:, data_store:, factory:)
|
|
45
|
+
@config = config
|
|
46
|
+
@data_store = data_store
|
|
47
|
+
@green_run_strategy = green_run_strategy
|
|
48
|
+
@yellow_run_strategy = yellow_run_strategy
|
|
49
|
+
@red_run_strategy = red_run_strategy
|
|
50
|
+
@factory = factory
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the current state of the light:
|
|
54
|
+
# * +Stoplight::State::LOCKED_GREEN+ -- light is locked green and allows all traffic
|
|
55
|
+
# * +Stoplight::State::LOCKED_RED+ -- light is locked red and blocks all traffic
|
|
56
|
+
# * +Stoplight::State::UNLOCKED+ -- light is not locked and follow the configured rules
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
def state
|
|
60
|
+
metadata.locked_state
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns current color:
|
|
64
|
+
# * +Stoplight::Color::GREEN+ -- circuit breaker is closed
|
|
65
|
+
# * +Stoplight::Color::RED+ -- circuit breaker is open
|
|
66
|
+
# * +Stoplight::Color::YELLOW+ -- circuit breaker is half-open
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# light = Stoplight('example')
|
|
70
|
+
# light.color #=> Color::GREEN
|
|
71
|
+
#
|
|
72
|
+
# @return [String] returns current light color
|
|
73
|
+
def color
|
|
74
|
+
metadata.color
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Runs the given block of code with this circuit breaker
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# light = Stoplight('example')
|
|
81
|
+
# light.run { 2/0 }
|
|
82
|
+
#
|
|
83
|
+
# @example Running with fallback
|
|
84
|
+
# light = Stoplight('example')
|
|
85
|
+
# light.run(->(error) { 0 }) { 1 / 0 } #=> 0
|
|
86
|
+
#
|
|
87
|
+
# @param fallback [Proc, nil] (nil) fallback code to run if the circuit breaker is open
|
|
88
|
+
# @raise [Stoplight::Error::RedLight]
|
|
89
|
+
# @return [any]
|
|
90
|
+
# @raise [Stoplight::Error::RedLight]
|
|
91
|
+
def run(fallback = nil, &code)
|
|
92
|
+
raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
|
|
93
|
+
|
|
94
|
+
metadata.then do |metadata|
|
|
95
|
+
strategy = state_strategy_factory(metadata.color)
|
|
96
|
+
strategy.execute(fallback, metadata:, &code)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Locks light in either +State::LOCKED_RED+ or +State::LOCKED_GREEN+
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# light = Stoplight('example-locked')
|
|
104
|
+
# light.lock(Stoplight::Color::RED)
|
|
105
|
+
#
|
|
106
|
+
# @param color [String] should be either +Color::RED+ or +Color::GREEN+
|
|
107
|
+
# @return [Stoplight::Light] returns locked light (circuit breaker)
|
|
108
|
+
def lock(color)
|
|
109
|
+
state = case color
|
|
110
|
+
when Color::RED then State::LOCKED_RED
|
|
111
|
+
when Color::GREEN then State::LOCKED_GREEN
|
|
112
|
+
else raise Error::IncorrectColor
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
data_store.set_state(config, state)
|
|
116
|
+
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Unlocks light and sets its state to State::UNLOCKED
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# light = Stoplight('example-locked')
|
|
124
|
+
# light.lock(Stoplight::Color::RED)
|
|
125
|
+
# light.unlock
|
|
126
|
+
#
|
|
127
|
+
# @return [Stoplight::Light] returns unlocked light (circuit breaker)
|
|
128
|
+
def unlock
|
|
129
|
+
data_store.set_state(config, State::UNLOCKED)
|
|
130
|
+
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Two lights considered equal if they have the same configuration.
|
|
135
|
+
#
|
|
136
|
+
# @param other [any]
|
|
137
|
+
# @return [Boolean]
|
|
138
|
+
def ==(other)
|
|
139
|
+
other.is_a?(self.class) && config == other.config && data_store == other.data_store &&
|
|
140
|
+
green_run_strategy == other.green_run_strategy && yellow_run_strategy == other.yellow_run_strategy &&
|
|
141
|
+
red_run_strategy == other.red_run_strategy && factory == other.factory
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Reconfigures the light with updated settings and returns a new instance.
|
|
145
|
+
#
|
|
146
|
+
# This method allows you to modify the configuration of a +Stoplight::Light+ object
|
|
147
|
+
# by providing a hash of settings. The original light remains unchanged, and a new
|
|
148
|
+
# light instance with the updated configuration is returned.
|
|
149
|
+
#
|
|
150
|
+
# @param settings [Hash] A hash of configuration options to update.
|
|
151
|
+
# @option settings [String] :name The name of the light.
|
|
152
|
+
# @option settings [Numeric] :cool_off_time The cool-off time in seconds before the light attempts recovery.
|
|
153
|
+
# @option settings [Numeric] :threshold The failure threshold to trigger the red state.
|
|
154
|
+
# @option settings [Numeric] :window_size The time window in seconds for counting failures.
|
|
155
|
+
# @option settings [Stoplight::DataStore::Base] :data_store The data store to use for persisting light state.
|
|
156
|
+
# @option settings [Array<Stoplight::Domain::AbstractStateTransitionNotifier>] :notifiers A list of notifiers to handle light events.
|
|
157
|
+
# @option settings [Proc] :error_notifier A custom error notifier to handle exceptions.
|
|
158
|
+
# @option settings [Array<StandardError>] :tracked_errors A list of errors to track for failure counting.
|
|
159
|
+
# @option settings [Array<StandardError>] :skipped_errors A list of errors to skip from failure counting.
|
|
160
|
+
# @return [Stoplight::Light] A new `Stoplight::Light` instance with the updated configuration.
|
|
161
|
+
#
|
|
162
|
+
# @example Reconfiguring a light with custom settings
|
|
163
|
+
# light = Stoplight('payment-api')
|
|
164
|
+
#
|
|
165
|
+
# # Create a light for invoices with a higher threshold
|
|
166
|
+
# invoices_light = light.with(tracked_errors: [TimeoutError], threshold: 10)
|
|
167
|
+
#
|
|
168
|
+
# # Create a light for payments with a lower threshold
|
|
169
|
+
# payment_light = light.with(threshold: 5)
|
|
170
|
+
#
|
|
171
|
+
# # Run the lights with their respective configurations
|
|
172
|
+
# invoices_light.run(->(error) { [] }) { call_invoices_api }
|
|
173
|
+
# payment_light.run(->(error) { nil }) { call_payment_api }
|
|
174
|
+
# @see +Stoplight()+
|
|
175
|
+
def with(**settings)
|
|
176
|
+
factory.build_with(**settings)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def state_strategy_factory(color)
|
|
182
|
+
case color
|
|
183
|
+
when Color::GREEN
|
|
184
|
+
green_run_strategy
|
|
185
|
+
when Color::YELLOW
|
|
186
|
+
yellow_run_strategy
|
|
187
|
+
else
|
|
188
|
+
red_run_strategy
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Stoplight::Domain::Metadata]
|
|
193
|
+
def metadata
|
|
194
|
+
data_store.get_metadata(config)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|