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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +288 -354
  3. data/lib/stoplight/admin/actions/action.rb +24 -0
  4. data/lib/stoplight/admin/actions/lock.rb +23 -0
  5. data/lib/stoplight/admin/actions/lock_all_green.rb +18 -0
  6. data/lib/stoplight/admin/actions/lock_green.rb +23 -0
  7. data/lib/stoplight/admin/actions/lock_red.rb +23 -0
  8. data/lib/stoplight/admin/actions/stats.rb +27 -0
  9. data/lib/stoplight/admin/actions/unlock.rb +23 -0
  10. data/lib/stoplight/admin/dependencies.rb +50 -0
  11. data/lib/stoplight/admin/helpers.rb +27 -0
  12. data/lib/stoplight/admin/lights_repository/light.rb +155 -0
  13. data/lib/stoplight/admin/lights_repository.rb +74 -0
  14. data/lib/stoplight/admin/lights_stats.rb +77 -0
  15. data/lib/stoplight/admin/views/_card.erb +120 -0
  16. data/lib/stoplight/admin/views/index.erb +36 -0
  17. data/lib/stoplight/admin/views/layout.erb +66 -0
  18. data/lib/stoplight/admin.rb +68 -0
  19. data/lib/stoplight/color.rb +3 -3
  20. data/lib/stoplight/config/config_provider.rb +62 -0
  21. data/lib/stoplight/config/library_default_config.rb +29 -0
  22. data/lib/stoplight/config/user_default_config.rb +83 -0
  23. data/lib/stoplight/data_store/base.rb +59 -33
  24. data/lib/stoplight/data_store/fail_safe.rb +105 -0
  25. data/lib/stoplight/data_store/memory.rb +257 -50
  26. data/lib/stoplight/data_store/redis/get_metadata.lua +38 -0
  27. data/lib/stoplight/data_store/redis/lua.rb +23 -0
  28. data/lib/stoplight/data_store/redis/record_failure.lua +36 -0
  29. data/lib/stoplight/data_store/redis/record_success.lua +35 -0
  30. data/lib/stoplight/data_store/redis/transition_to_green.lua +10 -0
  31. data/lib/stoplight/data_store/redis/transition_to_red.lua +10 -0
  32. data/lib/stoplight/data_store/redis/transition_to_yellow.lua +9 -0
  33. data/lib/stoplight/data_store/redis.rb +345 -106
  34. data/lib/stoplight/default.rb +11 -9
  35. data/lib/stoplight/error.rb +1 -13
  36. data/lib/stoplight/failure.rb +14 -13
  37. data/lib/stoplight/light/config.rb +118 -0
  38. data/lib/stoplight/light/configuration_builder_interface.rb +128 -0
  39. data/lib/stoplight/light/green_run_strategy.rb +53 -0
  40. data/lib/stoplight/light/red_run_strategy.rb +26 -0
  41. data/lib/stoplight/light/run_strategy.rb +30 -0
  42. data/lib/stoplight/light/yellow_run_strategy.rb +78 -0
  43. data/lib/stoplight/light.rb +164 -84
  44. data/lib/stoplight/metadata.rb +71 -0
  45. data/lib/stoplight/notifier/base.rb +14 -7
  46. data/lib/stoplight/notifier/fail_safe.rb +67 -0
  47. data/lib/stoplight/notifier/generic.rb +54 -5
  48. data/lib/stoplight/rspec/generic_notifier.rb +11 -12
  49. data/lib/stoplight/rspec.rb +1 -1
  50. data/lib/stoplight/state.rb +3 -3
  51. data/lib/stoplight/traffic_control/base.rb +35 -0
  52. data/lib/stoplight/traffic_control/consecutive_failures.rb +43 -0
  53. data/lib/stoplight/traffic_recovery/base.rb +51 -0
  54. data/lib/stoplight/traffic_recovery/single_success.rb +35 -0
  55. data/lib/stoplight/version.rb +1 -1
  56. data/lib/stoplight.rb +111 -51
  57. metadata +49 -98
  58. data/lib/stoplight/builder.rb +0 -70
  59. data/lib/stoplight/circuit_breaker.rb +0 -102
  60. data/lib/stoplight/configurable.rb +0 -95
  61. data/lib/stoplight/configuration.rb +0 -126
  62. data/lib/stoplight/light/deprecated.rb +0 -44
  63. data/lib/stoplight/light/lockable.rb +0 -45
  64. data/lib/stoplight/light/runnable.rb +0 -127
  65. data/lib/stoplight/notifier.rb +0 -6
  66. data/spec/spec_helper.rb +0 -22
  67. data/spec/stoplight/builder_spec.rb +0 -165
  68. data/spec/stoplight/circuit_breaker_spec.rb +0 -43
  69. data/spec/stoplight/color_spec.rb +0 -39
  70. data/spec/stoplight/configurable_spec.rb +0 -25
  71. data/spec/stoplight/data_store/base_spec.rb +0 -71
  72. data/spec/stoplight/data_store/memory_spec.rb +0 -22
  73. data/spec/stoplight/data_store/redis_spec.rb +0 -45
  74. data/spec/stoplight/data_store_spec.rb +0 -9
  75. data/spec/stoplight/default_spec.rb +0 -80
  76. data/spec/stoplight/error_spec.rb +0 -39
  77. data/spec/stoplight/failure_spec.rb +0 -108
  78. data/spec/stoplight/light/lockable_spec.rb +0 -93
  79. data/spec/stoplight/light/runnable_spec.rb +0 -38
  80. data/spec/stoplight/light_spec.rb +0 -156
  81. data/spec/stoplight/notifier/base_spec.rb +0 -18
  82. data/spec/stoplight/notifier/generic_spec.rb +0 -50
  83. data/spec/stoplight/notifier/io_spec.rb +0 -41
  84. data/spec/stoplight/notifier/logger_spec.rb +0 -75
  85. data/spec/stoplight/notifier_spec.rb +0 -9
  86. data/spec/stoplight/state_spec.rb +0 -39
  87. data/spec/stoplight/version_spec.rb +0 -9
  88. data/spec/stoplight_spec.rb +0 -32
  89. data/spec/support/configurable.rb +0 -69
  90. data/spec/support/data_store/base/clear_failures.rb +0 -24
  91. data/spec/support/data_store/base/clear_state.rb +0 -20
  92. data/spec/support/data_store/base/get_all.rb +0 -44
  93. data/spec/support/data_store/base/get_failures.rb +0 -30
  94. data/spec/support/data_store/base/get_state.rb +0 -7
  95. data/spec/support/data_store/base/names.rb +0 -29
  96. data/spec/support/data_store/base/record_failures.rb +0 -70
  97. data/spec/support/data_store/base/set_state.rb +0 -15
  98. data/spec/support/data_store/base/with_notification_lock.rb +0 -27
  99. data/spec/support/data_store/base.rb +0 -21
  100. data/spec/support/database_cleaner.rb +0 -26
  101. data/spec/support/exception_helpers.rb +0 -9
  102. data/spec/support/light/runnable/color.rb +0 -79
  103. data/spec/support/light/runnable/run.rb +0 -247
  104. data/spec/support/light/runnable/state.rb +0 -31
  105. 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
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Stoplight
4
4
  module Color
5
- GREEN = 'green'
6
- YELLOW = 'yellow'
7
- RED = 'red'
5
+ GREEN = "green"
6
+ YELLOW = "yellow"
7
+ RED = "red"
8
8
  end
9
9
  end
@@ -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
- # @return [Array<String>]
8
- def names
9
- raise NotImplementedError
10
- end
7
+ METRICS_RETENTION_TIME = 60 * 60 * 24 # 1 day
11
8
 
12
- # @param _light [Light]
13
- # @return [Array(Array<Failure>, String)]
14
- def get_all(_light)
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
- # @param _light [Light]
19
- # @return [Array<Failure>]
20
- def get_failures(_light)
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
- # @param _light [Light]
25
- # @param _failure [Failure]
26
- # @return [Fixnum]
27
- def record_failure(_light, _failure)
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
- # @param _light [Light]
32
- # @return [Array<Failure>]
33
- def clear_failures(_light)
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
- # @param _light [Light]
38
- # @return [String]
39
- def get_state(_light)
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
- # @param _light [Light]
44
- # @param _state [String]
45
- # @return [String]
46
- def set_state(_light, _state)
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
- # @param _light [Light]
51
- # @return [String]
52
- def clear_state(_light)
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
- # @param _light [Light]
57
- # @param _from_color [String]
58
- # @param _to_color [String]
59
- # @yield _block
60
- # @return [Void]
61
- def with_notification_lock(_light, _from_color, _to_color, &_block)
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