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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/stoplight/admin/views/layout.erb +3 -3
  4. data/lib/stoplight/admin.rb +4 -4
  5. data/lib/stoplight/domain/color.rb +11 -0
  6. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  7. data/lib/stoplight/domain/config.rb +55 -0
  8. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +17 -15
  9. data/lib/stoplight/domain/error.rb +42 -0
  10. data/lib/stoplight/domain/failure.rb +42 -0
  11. data/lib/stoplight/domain/light/configuration_builder_interface.rb +130 -0
  12. data/lib/stoplight/domain/light.rb +198 -0
  13. data/lib/stoplight/domain/light_factory.rb +75 -0
  14. data/lib/stoplight/domain/metadata.rb +65 -0
  15. data/lib/stoplight/domain/state.rb +11 -0
  16. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  17. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  18. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  19. data/lib/stoplight/domain/strategies/run_strategy.rb +27 -0
  20. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +98 -0
  21. data/lib/stoplight/domain/tracker/base.rb +41 -0
  22. data/lib/stoplight/domain/tracker/recovery_probe.rb +72 -0
  23. data/lib/stoplight/domain/tracker/request.rb +67 -0
  24. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  25. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +57 -0
  26. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  27. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  28. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +70 -0
  29. data/lib/stoplight/domain/traffic_recovery.rb +13 -0
  30. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  31. data/lib/stoplight/infrastructure/data_store/memory.rb +307 -0
  32. data/lib/stoplight/infrastructure/data_store/redis/lua.rb +25 -0
  33. data/lib/stoplight/infrastructure/data_store/redis.rb +478 -0
  34. data/lib/stoplight/infrastructure/dependency_injection/container.rb +249 -0
  35. data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +13 -0
  36. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  37. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  38. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  39. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  40. data/lib/stoplight/version.rb +1 -1
  41. data/lib/stoplight/wiring/container.rb +80 -0
  42. data/lib/stoplight/wiring/default.rb +28 -0
  43. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  44. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  45. data/lib/stoplight/{data_store/fail_safe.rb → wiring/fail_safe_data_store.rb} +22 -11
  46. data/lib/stoplight/{notifier/fail_safe.rb → wiring/fail_safe_notifier.rb} +22 -13
  47. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  48. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  49. data/lib/stoplight/wiring/light_factory.rb +188 -0
  50. data/lib/stoplight/wiring/public_api.rb +28 -0
  51. data/lib/stoplight/wiring/system_container.rb +9 -0
  52. data/lib/stoplight/wiring/system_light_factory.rb +17 -0
  53. data/lib/stoplight.rb +38 -28
  54. metadata +53 -43
  55. data/lib/stoplight/color.rb +0 -9
  56. data/lib/stoplight/config/dsl.rb +0 -97
  57. data/lib/stoplight/config/library_default_config.rb +0 -21
  58. data/lib/stoplight/config/system_config.rb +0 -10
  59. data/lib/stoplight/data_store/memory/sliding_window.rb +0 -77
  60. data/lib/stoplight/data_store/memory.rb +0 -285
  61. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  62. data/lib/stoplight/data_store/redis.rb +0 -446
  63. data/lib/stoplight/data_store.rb +0 -6
  64. data/lib/stoplight/default.rb +0 -30
  65. data/lib/stoplight/error.rb +0 -39
  66. data/lib/stoplight/failure.rb +0 -71
  67. data/lib/stoplight/light/config.rb +0 -112
  68. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  69. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  70. data/lib/stoplight/light/red_run_strategy.rb +0 -31
  71. data/lib/stoplight/light/run_strategy.rb +0 -32
  72. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  73. data/lib/stoplight/light.rb +0 -191
  74. data/lib/stoplight/metadata.rb +0 -99
  75. data/lib/stoplight/notifier/generic.rb +0 -79
  76. data/lib/stoplight/notifier/io.rb +0 -21
  77. data/lib/stoplight/notifier/logger.rb +0 -19
  78. data/lib/stoplight/state.rb +0 -9
  79. data/lib/stoplight/traffic_control/base.rb +0 -70
  80. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  81. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  82. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  83. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  84. data/lib/stoplight/traffic_recovery.rb +0 -11
  85. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/get_metadata.lua +0 -0
  86. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_failure.lua +0 -0
  87. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/record_success.lua +0 -0
  88. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_green.lua +0 -0
  89. /data/lib/stoplight/{data_store → infrastructure/data_store}/redis/transition_to_red.lua +0 -0
  90. /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: 297bbf636a9d6fa8e47b2241a3bfe0ccf357954044dc9cd02b79735012d92148
4
+ data.tar.gz: 7e539fac482dbde352b0e971b20d29968758c85017c2f917859d5130ef8ed7a1
5
5
  SHA512:
6
- metadata.gz: 9aa893f78f602c6d71f82f15deb41d6aa0bb4bc0ca37bf3756003699e2b158e1d3a857ee0f1fdd6cebcbb35fb787e5f515a36f89dd5560c000297302cd0e7e18
7
- data.tar.gz: 772938b653645a1ad3c8ca5e9e5958871c1b8802da6ce947d38fd9d5390fa9e336d8838941571569aed102cd0656c58389224b417bcf059f4569295c42894d6d
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>
@@ -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 necessery for running Stoplight Admin panel
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.default_config.data_store }
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
@@ -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.
@@ -15,24 +16,24 @@ module Stoplight
15
16
 
16
17
  # Retrieves metadata 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.
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::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)
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::Light::Config]
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::Light::Config]
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::Light::Config]
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::Light::Config]
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::Light::Config]
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