stoplight 5.4.0 → 5.6.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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/stoplight/admin/actions/remove.rb +23 -0
  4. data/lib/stoplight/admin/dependencies.rb +5 -0
  5. data/lib/stoplight/admin/lights_repository.rb +12 -3
  6. data/lib/stoplight/admin/views/_card.erb +13 -1
  7. data/lib/stoplight/admin/views/layout.erb +3 -3
  8. data/lib/stoplight/admin.rb +12 -4
  9. data/lib/stoplight/domain/color.rb +11 -0
  10. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  11. data/lib/stoplight/domain/config.rb +55 -0
  12. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +55 -17
  13. data/lib/stoplight/domain/error.rb +42 -0
  14. data/lib/stoplight/domain/failure.rb +44 -0
  15. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  16. data/lib/stoplight/domain/light.rb +197 -0
  17. data/lib/stoplight/domain/light_factory.rb +75 -0
  18. data/lib/stoplight/domain/metrics.rb +85 -0
  19. data/lib/stoplight/domain/state.rb +11 -0
  20. data/lib/stoplight/domain/state_snapshot.rb +57 -0
  21. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  22. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  23. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  24. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  25. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +99 -0
  26. data/lib/stoplight/domain/tracker/base.rb +41 -0
  27. data/lib/stoplight/domain/tracker/recovery_probe.rb +75 -0
  28. data/lib/stoplight/domain/tracker/request.rb +68 -0
  29. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  30. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +53 -0
  31. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  32. data/lib/stoplight/domain/traffic_recovery/base.rb +80 -0
  33. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +72 -0
  34. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  35. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  36. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  37. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  38. data/lib/stoplight/infrastructure/data_store/memory.rb +309 -0
  39. data/lib/stoplight/infrastructure/data_store/redis/get_metrics.lua +26 -0
  40. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  41. data/lib/stoplight/infrastructure/data_store/redis.rb +553 -0
  42. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  43. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  44. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  45. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  46. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  47. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  48. data/lib/stoplight/version.rb +1 -1
  49. data/lib/stoplight/wiring/container.rb +80 -0
  50. data/lib/stoplight/wiring/default.rb +28 -0
  51. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  52. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  53. data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +49 -14
  54. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  55. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  56. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  57. data/lib/stoplight/wiring/light_factory.rb +188 -0
  58. data/lib/stoplight/wiring/public_api.rb +28 -0
  59. data/lib/stoplight/wiring/system_container.rb +9 -0
  60. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  61. data/lib/stoplight.rb +38 -28
  62. metadata +57 -43
  63. data/lib/stoplight/color.rb +0 -9
  64. data/lib/stoplight/config/dsl.rb +0 -97
  65. data/lib/stoplight/config/library_default_config.rb +0 -21
  66. data/lib/stoplight/config/system_config.rb +0 -10
  67. data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
  68. data/lib/stoplight/data_store/memory.rb +0 -285
  69. data/lib/stoplight/data_store/redis/get_metadata.lua +0 -38
  70. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  71. data/lib/stoplight/data_store/redis.rb +0 -446
  72. data/lib/stoplight/data_store.rb +0 -6
  73. data/lib/stoplight/default.rb +0 -30
  74. data/lib/stoplight/error.rb +0 -39
  75. data/lib/stoplight/failure.rb +0 -71
  76. data/lib/stoplight/light/config.rb +0 -112
  77. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  78. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  79. data/lib/stoplight/light/red_run_strategy.rb +0 -31
  80. data/lib/stoplight/light/run_strategy.rb +0 -32
  81. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  82. data/lib/stoplight/light.rb +0 -191
  83. data/lib/stoplight/metadata.rb +0 -99
  84. data/lib/stoplight/notifier/generic.rb +0 -79
  85. data/lib/stoplight/notifier/io.rb +0 -21
  86. data/lib/stoplight/notifier/logger.rb +0 -19
  87. data/lib/stoplight/state.rb +0 -9
  88. data/lib/stoplight/traffic_control/base.rb +0 -70
  89. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  90. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  91. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  92. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  93. data/lib/stoplight/traffic_recovery.rb +0 -11
  94. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  95. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  96. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  97. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  98. /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: 2629a7cdd83dce508860ffe0c7fe3da4cad02eab605e0cb1b4f1e4c0f7db509d
4
- data.tar.gz: 50f13d3625ab327dd07815fd3da1a9384a0cf0f1d75af2db853b098c3b90ba2e
3
+ metadata.gz: f540f78d771a711da32d2e50cc150c4072cf805e4ef7e52e0b5ea07dd2435fea
4
+ data.tar.gz: 2b859ca65577026db5e1e5af4bc2900e178edc0e230b1cfc75728ffcadc9efc8
5
5
  SHA512:
6
- metadata.gz: 9aa893f78f602c6d71f82f15deb41d6aa0bb4bc0ca37bf3756003699e2b158e1d3a857ee0f1fdd6cebcbb35fb787e5f515a36f89dd5560c000297302cd0e7e18
7
- data.tar.gz: 772938b653645a1ad3c8ca5e9e5958871c1b8802da6ce947d38fd9d5390fa9e336d8838941571569aed102cd0656c58389224b417bcf059f4569295c42894d6d
6
+ metadata.gz: 88eaa1d335e4b956cc354d4c8295e46acd6f4dbb0188cc4cd5ecef99a13c4cf666e09e9c2d4c4f926f2ed0c5a582c9283341de028dc4489645cbde256d805504
7
+ data.tar.gz: d94325879c511730d6b53b40d13fed577e10119813ec3be5f18f92caccf42d1a886d8c0fe8d6c887ff3d96610f7ac2c53e3d992d25e257cf38fea33795260272
data/README.md CHANGED
@@ -534,7 +534,7 @@ class ApplicationController < ActionController::Base
534
534
 
535
535
  def stoplight(&block)
536
536
  Stoplight("#{params[:controller]}##{params[:action]}")
537
- .run(-> { render(nothing: true, status: :service_unavailable) }, &block)
537
+ .run(-> (*) { render(nothing: true, status: :service_unavailable) }, &block)
538
538
  end
539
539
  end
540
540
  ```
@@ -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
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ class Admin
5
+ module Actions
6
+ # This action removes a light's metadata from Redis
7
+ class Remove < Action
8
+ # @param params [Hash] query parameters
9
+ # @return [void]
10
+ def call(params)
11
+ light_names(params).each do |name|
12
+ lights_repository.remove(name)
13
+ end
14
+ end
15
+
16
+ private def light_names(params)
17
+ Array(params[:names])
18
+ .map { |name| CGI.unescape(name) }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -45,6 +45,11 @@ module Stoplight
45
45
  def green_all_action
46
46
  Stoplight::Admin::Actions::LockAllGreen.new(lights_repository: lights_repository)
47
47
  end
48
+
49
+ # @return [Stoplight::Admin::Actions::Remove]
50
+ def remove_action
51
+ Stoplight::Admin::Actions::Remove.new(lights_repository: lights_repository)
52
+ end
48
53
  end
49
54
  end
50
55
  end
@@ -53,16 +53,25 @@ module Stoplight
53
53
  build_light(name).unlock
54
54
  end
55
55
 
56
+ # @param name [String] removes light metadata by its name
57
+ # @return [void]
58
+ def remove(name)
59
+ light = build_light(name)
60
+
61
+ data_store.delete_light(light.config)
62
+ end
63
+
56
64
  private def load_light(name)
57
65
  light = build_light(name)
58
66
  # failures, state
59
- metadata = data_store.get_metadata(light.config)
67
+ state_snapshot = data_store.get_state_snapshot(light.config)
68
+ metrics = data_store.get_metrics(light.config)
60
69
 
61
70
  Light.new(
62
71
  name: name,
63
72
  color: light.color,
64
- state: metadata.locked_state,
65
- failures: [metadata.last_error].compact
73
+ state: state_snapshot.locked_state,
74
+ failures: [metrics.last_error].compact
66
75
  )
67
76
  end
68
77
 
@@ -1,6 +1,6 @@
1
1
  <div class="max-w-xl mr-5 p-6 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
2
2
  <div class="flex items-center">
3
- <% light_name = ERB::Util.html_escape(light.name) %>
3
+ <% light_name = CGI.escape(light.name) %>
4
4
 
5
5
  <div class="relative">
6
6
  <div class="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-<%= color %>-100 rounded-full dark:bg-<%= color %>-600">
@@ -83,6 +83,18 @@
83
83
  Lock Green
84
84
  </a>
85
85
  </li>
86
+ <li>
87
+ <a href="<%= url("/remove?names=#{light_name}") %>" data-turbo-method="post" data-turbo-confirm="Are you sure you want to remove this light?" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
88
+ <svg class="flex w-4 h-4 me-1.5 text-red-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">
89
+ <polyline points="3 6 5 6 21 6"/>
90
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
91
+ <path d="M10 11v6"/>
92
+ <path d="M14 11v6"/>
93
+ <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>
94
+ </svg>
95
+ Remove
96
+ </a>
97
+ </li>
86
98
  </ul>
87
99
  </div>
88
100
  </div>
@@ -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>
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi" # Ruby 3.2 needs this
4
+
3
5
  begin
4
6
  require "sinatra/base"
5
- require "sinatra/contrib"
6
7
  require "sinatra/json"
7
8
  rescue LoadError
8
9
  raise <<~WARN
9
- "sinatra" and "sinatra-contrib" gems are unavailable and necessery for running Stoplight Admin panel
10
+ "sinatra" and "sinatra-contrib" gems are unavailable and necessary for running Stoplight Admin panel
10
11
  Please add them to your Gemfile and run `bundle install`:
11
12
  gem "sinatra", required: false
12
13
  gem "sinatra-contrib", require: false
@@ -25,13 +26,14 @@ module Stoplight
25
26
  helpers Helpers
26
27
 
27
28
  set :protection, except: %i[json_csrf]
28
- set :data_store, proc { Stoplight.default_config.data_store }
29
+ set :data_store, proc { Stoplight.__stoplight__default_configuration.data_store }
29
30
  set :views, File.join(__dir__, "admin", "views")
31
+ set :nonce, proc { |request| }
30
32
 
31
33
  get "/" do
32
34
  lights, stats = dependencies.stats_action.call
33
35
 
34
- erb :index, locals: stats.merge(lights: lights)
36
+ erb :index, locals: stats.merge(lights: lights, nonce: settings.nonce(request))
35
37
  end
36
38
 
37
39
  get "/stats" do
@@ -63,5 +65,11 @@ module Stoplight
63
65
 
64
66
  redirect to("/")
65
67
  end
68
+
69
+ post "/remove" do
70
+ dependencies.remove_action.call(params)
71
+
72
+ redirect to("/")
73
+ end
66
74
  end
67
75
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Domain
5
+ module Color
6
+ GREEN = "green"
7
+ YELLOW = "yellow"
8
+ RED = "red"
9
+ end
10
+ end
11
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stoplight
4
- module Config
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 DataStore
4
+ module Domain
5
5
  # @abstract
6
- class Base
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.
@@ -13,26 +14,52 @@ module Stoplight
13
14
  raise NotImplementedError
14
15
  end
15
16
 
16
- # Retrieves metadata for a specific light configuration.
17
+ # Retrieves metrics for a specific light configuration.
17
18
  #
18
- # @param config [Stoplight::Light::Config] The light configuration.
19
- # @return [Stoplight::Metadata] The metadata associated with the light.
20
- def get_metadata(config)
19
+ # @param config [Stoplight::Domain::Config]
20
+ # @return [Stoplight::Domain::Metrics]
21
+ def get_metrics(config)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ # Retrieves recovery metrics for a specific light configuration.
26
+ #
27
+ # @param config [Stoplight::Domain::Config]
28
+ # @return [Stoplight::Domain::Metrics]
29
+ def get_recovery_metrics(config)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ # Retrieves State Snapshot for a specific light configuration.
34
+ #
35
+ # @param config [Stoplight::Domain::Config] The light configuration.
36
+ # @return [Stoplight::Domain::StateSnapshot]
37
+ def get_state_snapshot(config)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # Clears windowed metrics (successes/errors) to prevent
42
+ # stale failures from before recovery from affecting post-recovery decisions.
43
+ # Consecutive counts are intentionally preserved as they track current streaks.
44
+ #
45
+ # @param config [Stoplight::Domain::Config] The light configuration.
46
+ # @return [void]
47
+ def clear_windowed_metrics(config)
21
48
  raise NotImplementedError
22
49
  end
23
50
 
24
51
  # Records a failure for a specific light configuration.
25
52
  #
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)
53
+ # @param config [Stoplight::Domain::Config]
54
+ # @param exception [Exception]
55
+ # @return [void]
56
+ def record_failure(config, exception)
30
57
  raise NotImplementedError
31
58
  end
32
59
 
33
60
  # Records a success for a specific light configuration.
34
61
  #
35
- # @param config [Stoplight::Light::Config]
62
+ # @param config [Stoplight::Domain::Config]
36
63
  # @return [void]
37
64
  def record_success(config)
38
65
  raise NotImplementedError
@@ -40,24 +67,24 @@ module Stoplight
40
67
 
41
68
  # Records a failed recovery probe for a specific light configuration.
42
69
  #
43
- # @param config [Stoplight::Light::Config]
70
+ # @param config [Stoplight::Domain::Config]
44
71
  # @param failure [Failure]
45
- # @return [Stoplight::Metadata]
72
+ # @return [void]
46
73
  def record_recovery_probe_failure(config, failure)
47
74
  raise NotImplementedError
48
75
  end
49
76
 
50
77
  # Records a successful recovery probe for a specific light configuration.
51
78
  #
52
- # @param config [Stoplight::Light::Config]
53
- # @return [Stoplight::Metadata]
79
+ # @param config [Stoplight::Domain::Config]
80
+ # @return [void]
54
81
  def record_recovery_probe_success(config)
55
82
  raise NotImplementedError
56
83
  end
57
84
 
58
85
  # Sets the state of a specific light configuration.
59
86
  #
60
- # @param config [Stoplight::Light::Config]
87
+ # @param config [Stoplight::Domain::Config]
61
88
  # @param state [String] The new state to set.
62
89
  # @return [String] The state that was set.
63
90
  def set_state(config, state)
@@ -71,7 +98,7 @@ module Stoplight
71
98
  # is considered the "first" to perform the transition (and therefore responsible for
72
99
  # triggering notifications).
73
100
  #
74
- # @param config [Stoplight::Light::Config]
101
+ # @param config [Stoplight::Domain::Config]
75
102
  # @param color [String] The target color/state to transition to.
76
103
  # Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
77
104
  #
@@ -87,6 +114,17 @@ module Stoplight
87
114
  def transition_to_color(config, color)
88
115
  raise NotImplementedError
89
116
  end
117
+
118
+ # Deletes metadata (and related persistent state) for the given light.
119
+ #
120
+ # Implementations may choose to only remove metadata; metrics may expire via TTL.
121
+ #
122
+ # @param config [Stoplight::Domain::Config]
123
+ # @return [void]
124
+ def delete_light(config)
125
+ raise NotImplementedError
126
+ end
90
127
  end
128
+ # :nocov:
91
129
  end
92
130
  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,44 @@
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
+ alias_method :occurred_at, :time
33
+
34
+ # @param other [Failure]
35
+ # @return [Boolean]
36
+ def ==(other)
37
+ other.is_a?(self.class) &&
38
+ error_class == other.error_class &&
39
+ error_message == other.error_message &&
40
+ time == other.time
41
+ end
42
+ end
43
+ end
44
+ 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