stoplight 4.1.1 → 5.0.1
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 +288 -354
- data/lib/stoplight/admin/actions/action.rb +24 -0
- data/lib/stoplight/admin/actions/lock.rb +23 -0
- data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
- data/lib/stoplight/admin/actions/lock_green.rb +23 -0
- data/lib/stoplight/admin/actions/lock_red.rb +23 -0
- data/lib/stoplight/admin/actions/stats.rb +27 -0
- data/lib/stoplight/admin/actions/unlock.rb +23 -0
- data/lib/stoplight/admin/dependencies.rb +50 -0
- data/lib/stoplight/admin/helpers.rb +27 -0
- data/lib/stoplight/admin/lights_repository/light.rb +155 -0
- data/lib/stoplight/admin/lights_repository.rb +74 -0
- data/lib/stoplight/admin/lights_stats.rb +77 -0
- data/lib/stoplight/admin/views/_card.erb +120 -0
- data/lib/stoplight/admin/views/index.erb +36 -0
- data/lib/stoplight/admin/views/layout.erb +66 -0
- data/lib/stoplight/admin.rb +68 -0
- data/lib/stoplight/color.rb +3 -3
- data/lib/stoplight/config/config_provider.rb +62 -0
- data/lib/stoplight/config/library_default_config.rb +29 -0
- data/lib/stoplight/config/user_default_config.rb +83 -0
- data/lib/stoplight/data_store/base.rb +59 -33
- data/lib/stoplight/data_store/fail_safe.rb +105 -0
- data/lib/stoplight/data_store/memory.rb +257 -50
- data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
- data/lib/stoplight/data_store/redis/lua.rb +23 -0
- data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
- data/lib/stoplight/data_store/redis/record_success.lua +35 -0
- data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
- data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
- data/lib/stoplight/data_store/redis.rb +345 -106
- data/lib/stoplight/default.rb +11 -9
- data/lib/stoplight/error.rb +1 -13
- data/lib/stoplight/failure.rb +14 -13
- data/lib/stoplight/light/config.rb +118 -0
- data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
- data/lib/stoplight/light/green_run_strategy.rb +53 -0
- data/lib/stoplight/light/red_run_strategy.rb +26 -0
- data/lib/stoplight/light/run_strategy.rb +30 -0
- data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
- data/lib/stoplight/light.rb +164 -84
- data/lib/stoplight/metadata.rb +71 -0
- data/lib/stoplight/notifier/base.rb +14 -7
- data/lib/stoplight/notifier/fail_safe.rb +67 -0
- data/lib/stoplight/notifier/generic.rb +54 -5
- data/lib/stoplight/rspec/generic_notifier.rb +11 -12
- data/lib/stoplight/rspec.rb +1 -1
- data/lib/stoplight/state.rb +3 -3
- data/lib/stoplight/traffic_control/base.rb +35 -0
- data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
- data/lib/stoplight/traffic_recovery/base.rb +51 -0
- data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight.rb +111 -51
- metadata +49 -98
- data/lib/stoplight/builder.rb +0 -70
- data/lib/stoplight/circuit_breaker.rb +0 -102
- data/lib/stoplight/configurable.rb +0 -95
- data/lib/stoplight/configuration.rb +0 -126
- data/lib/stoplight/light/deprecated.rb +0 -44
- data/lib/stoplight/light/lockable.rb +0 -45
- data/lib/stoplight/light/runnable.rb +0 -127
- data/lib/stoplight/notifier.rb +0 -6
- data/spec/spec_helper.rb +0 -22
- data/spec/stoplight/builder_spec.rb +0 -165
- data/spec/stoplight/circuit_breaker_spec.rb +0 -43
- data/spec/stoplight/color_spec.rb +0 -39
- data/spec/stoplight/configurable_spec.rb +0 -25
- data/spec/stoplight/data_store/base_spec.rb +0 -71
- data/spec/stoplight/data_store/memory_spec.rb +0 -22
- data/spec/stoplight/data_store/redis_spec.rb +0 -45
- data/spec/stoplight/data_store_spec.rb +0 -9
- data/spec/stoplight/default_spec.rb +0 -80
- data/spec/stoplight/error_spec.rb +0 -39
- data/spec/stoplight/failure_spec.rb +0 -108
- data/spec/stoplight/light/lockable_spec.rb +0 -93
- data/spec/stoplight/light/runnable_spec.rb +0 -38
- data/spec/stoplight/light_spec.rb +0 -156
- data/spec/stoplight/notifier/base_spec.rb +0 -18
- data/spec/stoplight/notifier/generic_spec.rb +0 -50
- data/spec/stoplight/notifier/io_spec.rb +0 -41
- data/spec/stoplight/notifier/logger_spec.rb +0 -75
- data/spec/stoplight/notifier_spec.rb +0 -9
- data/spec/stoplight/state_spec.rb +0 -39
- data/spec/stoplight/version_spec.rb +0 -9
- data/spec/stoplight_spec.rb +0 -32
- data/spec/support/configurable.rb +0 -69
- data/spec/support/data_store/base/clear_failures.rb +0 -24
- data/spec/support/data_store/base/clear_state.rb +0 -20
- data/spec/support/data_store/base/get_all.rb +0 -44
- data/spec/support/data_store/base/get_failures.rb +0 -30
- data/spec/support/data_store/base/get_state.rb +0 -7
- data/spec/support/data_store/base/names.rb +0 -29
- data/spec/support/data_store/base/record_failures.rb +0 -70
- data/spec/support/data_store/base/set_state.rb +0 -15
- data/spec/support/data_store/base/with_notification_lock.rb +0 -27
- data/spec/support/data_store/base.rb +0 -21
- data/spec/support/database_cleaner.rb +0 -26
- data/spec/support/exception_helpers.rb +0 -9
- data/spec/support/light/runnable/color.rb +0 -79
- data/spec/support/light/runnable/run.rb +0 -247
- data/spec/support/light/runnable/state.rb +0 -31
- data/spec/support/light/runnable.rb +0 -5
@@ -0,0 +1,36 @@
|
|
1
|
+
<% if lights.empty? %>
|
2
|
+
<section class="bg-white dark:bg-gray-900">
|
3
|
+
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
|
4
|
+
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-50 dark:bg-blue-900/20">
|
5
|
+
<svg class="lucide lucide-unplug-icon lucide-unplug w-8 h-8 text-blue-600 dark:text-blue-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
6
|
+
<path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/>
|
7
|
+
</svg>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<h2 class="mb-3 text-2xl font-semibold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">
|
11
|
+
No lights found
|
12
|
+
</h2>
|
13
|
+
|
14
|
+
<div class="space-y-4">
|
15
|
+
<p class="mb-8 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 lg:px-48 dark:text-gray-400">
|
16
|
+
Ensure that your Stoplight data store is properly configured and that your Stoplight blocks have been run.
|
17
|
+
</p>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
21
|
+
<a href="javascript:location.reload();" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-900">
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-ccw-icon lucide-refresh-ccw w-3.5 h-3.5 me-2">
|
23
|
+
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/>
|
24
|
+
</svg>
|
25
|
+
Refresh Lights
|
26
|
+
</a>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
</section>
|
30
|
+
<% else %>
|
31
|
+
<section class="grid gap-4" style="grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));">
|
32
|
+
<% lights.each do |light| %>
|
33
|
+
<%= erb :_card, locals: { light:, color: light.color } %>
|
34
|
+
<% end %>
|
35
|
+
</section>
|
36
|
+
<% end %>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
|
+
|
7
|
+
<title>Stoplight Admin</title>
|
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">
|
11
|
+
document.addEventListener("turbo:load", () => {
|
12
|
+
window.initFlowbite()
|
13
|
+
})
|
14
|
+
</script>
|
15
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.css" />
|
16
|
+
</head>
|
17
|
+
<body>
|
18
|
+
<div class="antialiased bg-gray-50 dark:bg-gray-900">
|
19
|
+
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
20
|
+
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
21
|
+
<a href="<%= url('/') %>" class="flex items-center space-x-3 rtl:space-x-reverse">
|
22
|
+
🚦
|
23
|
+
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Stoplight Admin</span>
|
24
|
+
</a>
|
25
|
+
|
26
|
+
<button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
|
27
|
+
<span class="sr-only">Open main menu</span>
|
28
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
29
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
|
30
|
+
</svg>
|
31
|
+
</button>
|
32
|
+
|
33
|
+
<div class="hidden w-full md:block md:w-auto" id="navbar-default">
|
34
|
+
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
|
35
|
+
<% if count_red + count_yellow > 0 %>
|
36
|
+
<li>
|
37
|
+
<a href="<%= url('/green_all') %>" data-turbo-method="post" class="inline-flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
|
38
|
+
<svg class="flex w-4 h-4 me-1.5 text-green-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
39
|
+
<circle cx="12" cy="16" r="1"/>
|
40
|
+
<rect x="3" y="10" width="18" height="12" rx="2"/>
|
41
|
+
<path d="M7 10V7a5 5 0 0 1 10 0v3"/>
|
42
|
+
</svg>
|
43
|
+
Lock All Green
|
44
|
+
</a>
|
45
|
+
</li>
|
46
|
+
<% end %>
|
47
|
+
</ul>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</nav>
|
51
|
+
|
52
|
+
<main class="p-4 md:ml-64 h-auto">
|
53
|
+
<%= yield %>
|
54
|
+
</main>
|
55
|
+
|
56
|
+
<footer class="bg-white rounded-lg shadow-sm dark:bg-gray-900 m-4">
|
57
|
+
<div class="w-full max-w-screen-xl mx-auto p-4 md:py-8">
|
58
|
+
<p class="block text-sm text-gray-500 sm:text-center dark:text-gray-400 mb-2">All of the Lights -- cop lights, flashlights, spotlights, strobe lights...</p>
|
59
|
+
<p class="block text-sm text-gray-500 sm:text-center dark:text-gray-400">💡 🔦 🚨 🚥 💫</p>
|
60
|
+
</div>
|
61
|
+
</footer>
|
62
|
+
</div>
|
63
|
+
|
64
|
+
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
65
|
+
</body>
|
66
|
+
</html>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "sinatra"
|
5
|
+
require "sinatra/contrib"
|
6
|
+
require "sinatra/base"
|
7
|
+
require "sinatra/json"
|
8
|
+
rescue LoadError
|
9
|
+
raise <<~WARN
|
10
|
+
"sinatra" and "sinatra-contrib" gems are unavailable and necessery for running Stoplight Admin panel
|
11
|
+
Please add them to your Gemfile and run `bundle install`:
|
12
|
+
gem "sinatra", required: false
|
13
|
+
gem "sinatra-contrib", require: false
|
14
|
+
WARN
|
15
|
+
end
|
16
|
+
|
17
|
+
module Stoplight
|
18
|
+
class Admin < Sinatra::Base
|
19
|
+
COLORS = [
|
20
|
+
GREEN = Stoplight::Color::GREEN,
|
21
|
+
YELLOW = Stoplight::Color::YELLOW,
|
22
|
+
RED = Stoplight::Color::RED
|
23
|
+
].freeze
|
24
|
+
private_constant :COLORS
|
25
|
+
|
26
|
+
helpers Helpers
|
27
|
+
|
28
|
+
set :protection, except: %i[json_csrf]
|
29
|
+
set :data_store, proc { Stoplight.config_provider.data_store }
|
30
|
+
set :views, File.join(__dir__, "admin", "views")
|
31
|
+
|
32
|
+
get "/" do
|
33
|
+
lights, stats = dependencies.stats_action.call
|
34
|
+
|
35
|
+
erb :index, locals: stats.merge(lights: lights)
|
36
|
+
end
|
37
|
+
|
38
|
+
get "/stats" do
|
39
|
+
lights, stats = dependencies.stats_action.call
|
40
|
+
|
41
|
+
json({stats: stats, lights: lights.map(&:as_json)})
|
42
|
+
end
|
43
|
+
|
44
|
+
post "/unlock" do
|
45
|
+
dependencies.unlock_action.call(params)
|
46
|
+
|
47
|
+
redirect to("/")
|
48
|
+
end
|
49
|
+
|
50
|
+
post "/green" do
|
51
|
+
dependencies.green_action.call(params)
|
52
|
+
|
53
|
+
redirect to("/")
|
54
|
+
end
|
55
|
+
|
56
|
+
post "/red" do
|
57
|
+
dependencies.red_action.call(params)
|
58
|
+
|
59
|
+
redirect to("/")
|
60
|
+
end
|
61
|
+
|
62
|
+
post "/green_all" do
|
63
|
+
dependencies.green_all_action.call
|
64
|
+
|
65
|
+
redirect to("/")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/stoplight/color.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module Config
|
5
|
+
# Provides configuration for a Stoplight light by its name.
|
6
|
+
#
|
7
|
+
# It combines settings from three sources in the following order of precedence:
|
8
|
+
# 1. **Settings Overrides**: Explicit settings passed as arguments to +#provide+ method.
|
9
|
+
# 2. **User-level Default Settings**: Settings defined using the +Stoplight.configure+ method.
|
10
|
+
# 4. **Library-Level Default Settings**: Default settings defined in the +Stoplight::Config::UserDefaultConfig+ module.
|
11
|
+
#
|
12
|
+
# The settings are merged in this order, with higher-precedence settings overriding lower-precedence ones.
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
class ConfigProvider
|
16
|
+
# @!attribute [r] default_settings
|
17
|
+
# @return [Hash]
|
18
|
+
private attr_reader :default_settings
|
19
|
+
|
20
|
+
# @param user_default_config [Stoplight::Config::UserDefaultConfig]
|
21
|
+
# @param library_default_config [Stoplight::Config::LibraryDefaultConfig]
|
22
|
+
# @raise [Error::ConfigurationError] if both user_default_config and legacy_config are not empty
|
23
|
+
def initialize(user_default_config:, library_default_config:)
|
24
|
+
@default_settings = library_default_config.to_h.merge(
|
25
|
+
user_default_config.to_h
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Stoplight::DataStore::Base]
|
30
|
+
def data_store
|
31
|
+
default_settings.fetch(:data_store)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a configuration for a specific light with the given name and settings overrides.
|
35
|
+
#
|
36
|
+
# @param light_name [Symbol, String] The name of the light.
|
37
|
+
# @param settings_overrides [Hash] The settings to override.
|
38
|
+
# @see +Stoplight()+
|
39
|
+
# @return [Stoplight::Light::Config] The configuration for the specified light.
|
40
|
+
# @raise [Error::ConfigurationError]
|
41
|
+
def provide(light_name, **settings_overrides)
|
42
|
+
raise Error::ConfigurationError, <<~ERROR if settings_overrides.has_key?(:name)
|
43
|
+
The +name+ setting cannot be overridden in the configuration.
|
44
|
+
ERROR
|
45
|
+
|
46
|
+
settings = default_settings.merge(settings_overrides, {name: light_name})
|
47
|
+
Light::Config.new(**settings)
|
48
|
+
end
|
49
|
+
|
50
|
+
def inspect
|
51
|
+
"#<#{self.class.name} " \
|
52
|
+
"cool_off_time=#{default_settings[:cool_off_time]}, " \
|
53
|
+
"threshold=#{default_settings[:threshold]}, " \
|
54
|
+
"window_size=#{default_settings[:window_size]}, " \
|
55
|
+
"tracked_errors=#{default_settings[:tracked_errors].join(",")}, " \
|
56
|
+
"skipped_errors=#{default_settings[:skipped_errors].join(",")}, " \
|
57
|
+
"data_store=#{default_settings[:data_store].class.name}" \
|
58
|
+
">"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module Config
|
5
|
+
# Provides default settings for the Stoplight library.
|
6
|
+
# @api private
|
7
|
+
class LibraryDefaultConfig
|
8
|
+
DEFAULT_SETTINGS = {
|
9
|
+
cool_off_time: Stoplight::Default::COOL_OFF_TIME,
|
10
|
+
data_store: Stoplight::Default::DATA_STORE,
|
11
|
+
error_notifier: Stoplight::Default::ERROR_NOTIFIER,
|
12
|
+
notifiers: Stoplight::Default::NOTIFIERS,
|
13
|
+
threshold: Stoplight::Default::THRESHOLD,
|
14
|
+
window_size: Stoplight::Default::WINDOW_SIZE,
|
15
|
+
tracked_errors: Stoplight::Default::TRACKED_ERRORS,
|
16
|
+
skipped_errors: Stoplight::Default::SKIPPED_ERRORS,
|
17
|
+
traffic_control: Stoplight::Default::TRAFFIC_CONTROL,
|
18
|
+
traffic_recovery: Stoplight::Default::TRAFFIC_RECOVERY
|
19
|
+
}.freeze
|
20
|
+
private_constant :DEFAULT_SETTINGS
|
21
|
+
|
22
|
+
# Returns library default settings.
|
23
|
+
# @return [Hash]
|
24
|
+
def to_h
|
25
|
+
DEFAULT_SETTINGS
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Stoplight
|
6
|
+
module Config
|
7
|
+
# Represents user-defined default configuration for Stoplight.
|
8
|
+
#
|
9
|
+
# This class allows users to define default settings for various Stoplight
|
10
|
+
# parameters, such as cool-off time, data store, error notifier, and more.
|
11
|
+
# TODO: add evaluation/recovery strategy support
|
12
|
+
class UserDefaultConfig
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
# @!attribute [w] cool_off_time
|
16
|
+
# @return [Integer, nil] The default cool-off time in seconds.
|
17
|
+
attr_writer :cool_off_time
|
18
|
+
|
19
|
+
# @!attribute [w] error_notifier
|
20
|
+
# @return [Proc, nil] The default error notifier (callable object).
|
21
|
+
attr_writer :error_notifier
|
22
|
+
|
23
|
+
# @!attribute [r] notifiers
|
24
|
+
# @return [Array<Stoplight::Notifier::Base>] The default list of notifiers.
|
25
|
+
attr_reader :notifiers
|
26
|
+
|
27
|
+
# @!attribute [w] threshold
|
28
|
+
# @return [Integer, nil] The default failure threshold to trip the circuit breaker.
|
29
|
+
attr_writer :threshold
|
30
|
+
|
31
|
+
# @!attribute [w] window_size
|
32
|
+
# @return [Integer, nil] The default size of the rolling window for failure tracking.
|
33
|
+
attr_writer :window_size
|
34
|
+
|
35
|
+
# @!attribute [w] tracked_errors
|
36
|
+
# @return [Array<Class>, nil] The default list of errors to track.
|
37
|
+
attr_writer :tracked_errors
|
38
|
+
|
39
|
+
# @!attribute [w] skipped_errors
|
40
|
+
# @return [Array<Class>, nil] The default list of errors to skip.
|
41
|
+
attr_writer :skipped_errors
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
# This allows users appending notifiers to the default list,
|
45
|
+
# while still allowing them to override the default list.
|
46
|
+
@notifiers = Default::NOTIFIERS
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param value [Stoplight::DataStore::Base]
|
50
|
+
# @return [Stoplight::DataStore::Base] The default data store instance.
|
51
|
+
def data_store=(value)
|
52
|
+
@data_store = DataStore::FailSafe.wrap(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param value [Array<Stoplight::Notifier::Base>]
|
56
|
+
# @return [Array<Stoplight::Notifier::FailSafe>]
|
57
|
+
def notifiers=(value)
|
58
|
+
@notifiers = value.map { |notifier| Notifier::FailSafe.wrap(notifier) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Converts the user-defined configuration to a hash.
|
62
|
+
#
|
63
|
+
# @return [Hash] A hash representation of the configuration, excluding nil values.
|
64
|
+
# @api private
|
65
|
+
def to_h
|
66
|
+
{
|
67
|
+
cool_off_time: @cool_off_time,
|
68
|
+
data_store: @data_store,
|
69
|
+
error_notifier: @error_notifier,
|
70
|
+
notifiers: (@notifiers == Default::NOTIFIERS) ? nil : @notifiers, # This is to avoid conflicts with legacy config
|
71
|
+
threshold: @threshold,
|
72
|
+
window_size: @window_size,
|
73
|
+
tracked_errors: @tracked_errors,
|
74
|
+
skipped_errors: @skipped_errors
|
75
|
+
}.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Boolean] True if the configuration hash is not empty, false otherwise.
|
79
|
+
# @api private
|
80
|
+
def_delegator :to_h, :any?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -4,61 +4,87 @@ module Stoplight
|
|
4
4
|
module DataStore
|
5
5
|
# @abstract
|
6
6
|
class Base
|
7
|
-
#
|
8
|
-
def names
|
9
|
-
raise NotImplementedError
|
10
|
-
end
|
7
|
+
METRICS_RETENTION_TIME = 60 * 60 * 24 # 1 day
|
11
8
|
|
12
|
-
#
|
13
|
-
#
|
14
|
-
|
9
|
+
# Retrieves the names of all lights stored in the data store.
|
10
|
+
#
|
11
|
+
# @return [Array<String>] An array of light names.
|
12
|
+
def names
|
15
13
|
raise NotImplementedError
|
16
14
|
end
|
17
15
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
16
|
+
# Retrieves metadata for a specific light configuration.
|
17
|
+
#
|
18
|
+
# @param config [Stoplight::Light::Config] The light configuration.
|
19
|
+
# @return [Stoplight::Metadata] The metadata associated with the light.
|
20
|
+
def get_metadata(config)
|
21
21
|
raise NotImplementedError
|
22
22
|
end
|
23
23
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# @
|
27
|
-
|
24
|
+
# Records a failure for a specific light configuration.
|
25
|
+
#
|
26
|
+
# @param config [Stoplight::Light::Config]
|
27
|
+
# @param failure [Failure] The failure to record.
|
28
|
+
# @return [Stoplight::Metadata] The metadata associated with the light.
|
29
|
+
def record_failure(config, failure)
|
28
30
|
raise NotImplementedError
|
29
31
|
end
|
30
32
|
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
33
|
+
# Records a success for a specific light configuration.
|
34
|
+
#
|
35
|
+
# @param config [Stoplight::Light::Config]
|
36
|
+
# @return [void]
|
37
|
+
def record_success(config)
|
34
38
|
raise NotImplementedError
|
35
39
|
end
|
36
40
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
41
|
+
# Records a failed recovery probe for a specific light configuration.
|
42
|
+
#
|
43
|
+
# @param config [Stoplight::Light::Config]
|
44
|
+
# @param failure [Failure]
|
45
|
+
# @return [Stoplight::Metadata]
|
46
|
+
def record_recovery_probe_failure(config, failure)
|
40
47
|
raise NotImplementedError
|
41
48
|
end
|
42
49
|
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# @
|
46
|
-
|
50
|
+
# Records a successful recovery probe for a specific light configuration.
|
51
|
+
#
|
52
|
+
# @param config [Stoplight::Light::Config]
|
53
|
+
# @return [Stoplight::Metadata]
|
54
|
+
def record_recovery_probe_success(config)
|
47
55
|
raise NotImplementedError
|
48
56
|
end
|
49
57
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
|
58
|
+
# Sets the state of a specific light configuration.
|
59
|
+
#
|
60
|
+
# @param config [Stoplight::Light::Config]
|
61
|
+
# @param state [String] The new state to set.
|
62
|
+
# @return [String] The state that was set.
|
63
|
+
def set_state(config, state)
|
53
64
|
raise NotImplementedError
|
54
65
|
end
|
55
66
|
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
|
67
|
+
# Transitions the Stoplight to the specified color.
|
68
|
+
#
|
69
|
+
# This method performs a color transition operation that works across distributed instances
|
70
|
+
# of the light. It ensures that in a multi-instance environment, only one instance
|
71
|
+
# is considered the "first" to perform the transition (and therefore responsible for
|
72
|
+
# triggering notifications).
|
73
|
+
#
|
74
|
+
# @param config [Stoplight::Light::Config]
|
75
|
+
# @param color [String] The target color/state to transition to.
|
76
|
+
# Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
|
77
|
+
#
|
78
|
+
# @return [Boolean] Returns +true+ if this instance was the first to perform this specific transition
|
79
|
+
# (and should therefore trigger notifications). Returns +false+ if another instance already
|
80
|
+
# initiated this transition.
|
81
|
+
#
|
82
|
+
# @note In distributed environments with multiple instances, race conditions can occur when instances
|
83
|
+
# attempt conflicting transitions simultaneously (e.g., one instance tries to transition from
|
84
|
+
# YELLOW to GREEN while another tries YELLOW to RED). The implementation handles this, but
|
85
|
+
# be aware that the last operation may determine the final color of the light.
|
86
|
+
#
|
87
|
+
def transition_to_color(config, color)
|
62
88
|
raise NotImplementedError
|
63
89
|
end
|
64
90
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stoplight
|
4
|
+
module DataStore
|
5
|
+
# A wrapper around a data store that provides fail-safe mechanisms using a
|
6
|
+
# circuit breaker. It ensures that operations on the data store can gracefully
|
7
|
+
# handle failures by falling back to default values when necessary.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class FailSafe < Base
|
11
|
+
# @!attribute [r] data_store
|
12
|
+
# @return [Stoplight::DataStore::Base] The underlying data store being wrapped.
|
13
|
+
protected attr_reader :data_store
|
14
|
+
|
15
|
+
# @!attribute [r] circuit_breaker
|
16
|
+
# @return [Stoplight] The circuit breaker used to handle failures.
|
17
|
+
private attr_reader :circuit_breaker
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Wraps a data store with fail-safe mechanisms.
|
21
|
+
#
|
22
|
+
# @param data_store [Stoplight::DataStore::Base] The data store to wrap.
|
23
|
+
# @return [Stoplight::DataStore::Base, FailSafe] The original data store if it is already
|
24
|
+
# a +Memory+ or +FailSafe+ instance, otherwise a new +FailSafe+ instance.
|
25
|
+
def wrap(data_store)
|
26
|
+
case data_store
|
27
|
+
when Memory, FailSafe
|
28
|
+
data_store
|
29
|
+
else
|
30
|
+
new(data_store)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param data_store [Stoplight::DataStore::Base]
|
36
|
+
def initialize(data_store)
|
37
|
+
@data_store = data_store
|
38
|
+
@circuit_breaker = Stoplight("stoplight:data_store:fail_safe:#{data_store.class.name}", data_store: Default::DATA_STORE)
|
39
|
+
end
|
40
|
+
|
41
|
+
def names
|
42
|
+
with_fallback([]) do
|
43
|
+
data_store.names
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_metadata(config)
|
48
|
+
with_fallback(Metadata.new, config) do
|
49
|
+
data_store.get_metadata(config)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def record_failure(config, failure)
|
54
|
+
with_fallback(nil, config) do
|
55
|
+
data_store.record_failure(config, failure)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def record_success(config, **args)
|
60
|
+
with_fallback(nil, config) do
|
61
|
+
data_store.record_success(config, **args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def record_recovery_probe_success(config, **args)
|
66
|
+
with_fallback(nil, config) do
|
67
|
+
data_store.record_recovery_probe_success(config, **args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def record_recovery_probe_failure(config, failure)
|
72
|
+
with_fallback(nil, config) do
|
73
|
+
data_store.record_recovery_probe_failure(config, failure)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_state(config, state)
|
78
|
+
with_fallback(State::UNLOCKED, config) do
|
79
|
+
data_store.set_state(config, state)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def transition_to_color(config, color)
|
84
|
+
with_fallback(false, config) do
|
85
|
+
data_store.transition_to_color(config, color)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def ==(other)
|
90
|
+
other.is_a?(self.class) && other.data_store == data_store
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param default [Object, nil]
|
94
|
+
# @param config [Stoplight::Light::Config]
|
95
|
+
private def with_fallback(default = nil, config = nil, &code)
|
96
|
+
fallback = proc do |error|
|
97
|
+
config.error_notifier.call(error) if config && error
|
98
|
+
default
|
99
|
+
end
|
100
|
+
|
101
|
+
circuit_breaker.run(fallback, &code)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|