stoplight 5.3.8 → 5.7.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -2
  3. data/lib/generators/stoplight/install/templates/stoplight.rb.erb +2 -0
  4. data/lib/stoplight/admin/actions/remove.rb +23 -0
  5. data/lib/stoplight/admin/dependencies.rb +6 -1
  6. data/lib/stoplight/admin/helpers.rb +10 -5
  7. data/lib/stoplight/admin/lights_repository.rb +26 -14
  8. data/lib/stoplight/admin/views/_card.erb +13 -1
  9. data/lib/stoplight/admin/views/layout.erb +3 -3
  10. data/lib/stoplight/admin.rb +13 -4
  11. data/lib/stoplight/common/deprecations.rb +11 -0
  12. data/lib/stoplight/domain/color.rb +11 -0
  13. data/lib/stoplight/{config → domain}/compatibility_result.rb +1 -1
  14. data/lib/stoplight/domain/config.rb +59 -0
  15. data/lib/stoplight/{data_store/base.rb → domain/data_store.rb} +71 -17
  16. data/lib/stoplight/domain/error.rb +42 -0
  17. data/lib/stoplight/domain/failure.rb +44 -0
  18. data/lib/stoplight/domain/light/configuration_builder_interface.rb +234 -0
  19. data/lib/stoplight/domain/light.rb +208 -0
  20. data/lib/stoplight/domain/light_factory.rb +75 -0
  21. data/lib/stoplight/domain/metrics.rb +64 -0
  22. data/lib/stoplight/domain/recovery_lock_token.rb +15 -0
  23. data/lib/stoplight/domain/state.rb +11 -0
  24. data/lib/stoplight/domain/state_snapshot.rb +57 -0
  25. data/lib/stoplight/{notifier/base.rb → domain/state_transition_notifier.rb} +5 -4
  26. data/lib/stoplight/domain/storage/metrics.rb +42 -0
  27. data/lib/stoplight/domain/storage/recovery_lock.rb +56 -0
  28. data/lib/stoplight/domain/storage/state.rb +87 -0
  29. data/lib/stoplight/domain/strategies/green_run_strategy.rb +69 -0
  30. data/lib/stoplight/domain/strategies/red_run_strategy.rb +41 -0
  31. data/lib/stoplight/domain/strategies/run_strategy.rb +22 -0
  32. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +125 -0
  33. data/lib/stoplight/domain/tracker/base.rb +12 -0
  34. data/lib/stoplight/domain/tracker/recovery_probe.rb +76 -0
  35. data/lib/stoplight/domain/tracker/request.rb +72 -0
  36. data/lib/stoplight/domain/traffic_control/base.rb +74 -0
  37. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +53 -0
  38. data/lib/stoplight/domain/traffic_control/error_rate.rb +51 -0
  39. data/lib/stoplight/domain/traffic_recovery/base.rb +79 -0
  40. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +66 -0
  41. data/lib/stoplight/domain/traffic_recovery.rb +12 -0
  42. data/lib/stoplight/infrastructure/data_store/fail_safe.rb +164 -0
  43. data/lib/stoplight/infrastructure/data_store/memory/metrics.rb +27 -0
  44. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_store.rb +54 -0
  45. data/lib/stoplight/infrastructure/data_store/memory/recovery_lock_token.rb +20 -0
  46. data/lib/stoplight/infrastructure/data_store/memory/sliding_window.rb +79 -0
  47. data/lib/stoplight/infrastructure/data_store/memory/state.rb +21 -0
  48. data/lib/stoplight/infrastructure/data_store/memory.rb +338 -0
  49. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/get_metrics.lua +26 -0
  50. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_failure.lua +27 -0
  51. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/record_recovery_probe_success.lua +23 -0
  52. data/lib/stoplight/infrastructure/data_store/redis/lua_scripts/release_lock.lua +6 -0
  53. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_store.rb +73 -0
  54. data/lib/stoplight/infrastructure/data_store/redis/recovery_lock_token.rb +35 -0
  55. data/lib/stoplight/infrastructure/data_store/redis/scripting.rb +71 -0
  56. data/lib/stoplight/infrastructure/data_store/redis.rb +524 -0
  57. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +62 -0
  58. data/lib/stoplight/infrastructure/notifier/generic.rb +90 -0
  59. data/lib/stoplight/infrastructure/notifier/io.rb +23 -0
  60. data/lib/stoplight/infrastructure/notifier/logger.rb +21 -0
  61. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +48 -0
  62. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +36 -0
  63. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +55 -0
  64. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +55 -0
  65. data/lib/stoplight/rspec/generic_notifier.rb +1 -1
  66. data/lib/stoplight/version.rb +1 -1
  67. data/lib/stoplight/wiring/data_store/base.rb +11 -0
  68. data/lib/stoplight/wiring/data_store/memory.rb +10 -0
  69. data/lib/stoplight/wiring/data_store/redis.rb +25 -0
  70. data/lib/stoplight/wiring/default.rb +28 -0
  71. data/lib/stoplight/{config/user_default_config.rb → wiring/default_configuration.rb} +24 -31
  72. data/lib/stoplight/wiring/default_factory_builder.rb +25 -0
  73. data/lib/stoplight/wiring/light/default_config.rb +18 -0
  74. data/lib/stoplight/wiring/light/system_config.rb +11 -0
  75. data/lib/stoplight/wiring/light_builder.rb +185 -0
  76. data/lib/stoplight/wiring/light_factory/compatibility_validator.rb +55 -0
  77. data/lib/stoplight/wiring/light_factory/config_normalizer.rb +71 -0
  78. data/lib/stoplight/wiring/light_factory/configuration_pipeline.rb +72 -0
  79. data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
  80. data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
  81. data/lib/stoplight/wiring/light_factory.rb +101 -0
  82. data/lib/stoplight/wiring/notifier_factory.rb +26 -0
  83. data/lib/stoplight/wiring/public_api.rb +29 -0
  84. data/lib/stoplight.rb +55 -30
  85. metadata +92 -42
  86. data/lib/stoplight/color.rb +0 -9
  87. data/lib/stoplight/config/dsl.rb +0 -97
  88. data/lib/stoplight/config/library_default_config.rb +0 -21
  89. data/lib/stoplight/config/system_config.rb +0 -7
  90. data/lib/stoplight/data_store/fail_safe.rb +0 -113
  91. data/lib/stoplight/data_store/memory.rb +0 -311
  92. data/lib/stoplight/data_store/redis/get_metadata.lua +0 -38
  93. data/lib/stoplight/data_store/redis/lua.rb +0 -23
  94. data/lib/stoplight/data_store/redis.rb +0 -449
  95. data/lib/stoplight/data_store.rb +0 -6
  96. data/lib/stoplight/default.rb +0 -30
  97. data/lib/stoplight/error.rb +0 -10
  98. data/lib/stoplight/failure.rb +0 -71
  99. data/lib/stoplight/light/config.rb +0 -111
  100. data/lib/stoplight/light/configuration_builder_interface.rb +0 -128
  101. data/lib/stoplight/light/green_run_strategy.rb +0 -54
  102. data/lib/stoplight/light/red_run_strategy.rb +0 -27
  103. data/lib/stoplight/light/run_strategy.rb +0 -32
  104. data/lib/stoplight/light/yellow_run_strategy.rb +0 -94
  105. data/lib/stoplight/light.rb +0 -191
  106. data/lib/stoplight/metadata.rb +0 -99
  107. data/lib/stoplight/notifier/fail_safe.rb +0 -70
  108. data/lib/stoplight/notifier/generic.rb +0 -79
  109. data/lib/stoplight/notifier/io.rb +0 -21
  110. data/lib/stoplight/notifier/logger.rb +0 -19
  111. data/lib/stoplight/state.rb +0 -9
  112. data/lib/stoplight/traffic_control/base.rb +0 -70
  113. data/lib/stoplight/traffic_control/consecutive_errors.rb +0 -55
  114. data/lib/stoplight/traffic_control/error_rate.rb +0 -49
  115. data/lib/stoplight/traffic_recovery/base.rb +0 -75
  116. data/lib/stoplight/traffic_recovery/consecutive_successes.rb +0 -68
  117. data/lib/stoplight/traffic_recovery.rb +0 -11
  118. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_failure.lua +0 -0
  119. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/record_success.lua +0 -0
  120. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_green.lua +0 -0
  121. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_red.lua +0 -0
  122. /data/lib/stoplight/{data_store/redis → infrastructure/data_store/redis/lua_scripts}/transition_to_yellow.lua +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e14702003b76fde01b28e5266b392f5d55b2924c83365a0ab122f11aebf2e4a7
4
- data.tar.gz: e9c1b348b2637aa66408de62a2bbbc24b9b73927ca9712a8b4137446e336cd0a
3
+ metadata.gz: d96e1b5dcce81c642d059b07b2e274b8c7dbd7f246b86ee25ce40567eaba8418
4
+ data.tar.gz: e6b04f0ff592f89f345ff3f9ec9e26b86463ff0baeb56ef752cbd00b7f8952b8
5
5
  SHA512:
6
- metadata.gz: 1601dd8a7fae2e9c8f89a52556c8f931d3712d71648300aef0e3dae226c9998b7b1fa35e7fa0ce151d943e961473e012ca382b718bb50b44000f5a43e4d667b8
7
- data.tar.gz: 7c4e9696009e01bea1908cd3212d4625cc0ac3e03205d1cd69e4471f9aec4f3add7f101e58b393df210658e6be2c2d42b5d4a2ddedd954f832053505e291f232
6
+ metadata.gz: 01c5c2356f84eaeade94edba7113e700682911558d165b8b60186abbe0cda3d0f453427b8ab6e96f4ab51948e3ec5a109e53e1b51cc15c46c3fd79aad4865768
7
+ data.tar.gz: 94633b744ef88219f66289d9aae91bbb35b013bbea82c84f0f253abd15d4e5bd7ea5a123c39dffc36911479b1663cee71f786110be0ee15c3c8f56d035032e76
data/README.md CHANGED
@@ -99,6 +99,19 @@ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
99
99
  light.color # => "red"
100
100
  ```
101
101
 
102
+ The `Stoplight::Error::RedLight` provides metadata about the error:
103
+
104
+ ```ruby
105
+ def run_request
106
+ light = Stoplight("Example", cool_off_time: 10)
107
+ light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight
108
+ rescue Stoplight::Error::RedLight => error
109
+ puts error.light_name #=> "Example"
110
+ puts error.cool_off_time #=> 10
111
+ puts error.retry_after #=> Absolute Time after which a recovery attempt can occur (e.g., "2025-10-21 15:39:50.672414 +0600")
112
+ end
113
+ ```
114
+
102
115
  After one minute, the light transitions to yellow, allowing a test execution:
103
116
 
104
117
  ```ruby
@@ -131,6 +144,8 @@ receives `nil`. In both cases, the return value of the fallback becomes the retu
131
144
 
132
145
  Stoplight comes with a built-in Admin Panel that can track all active Lights and manually lock them in the desired state (`Green` or `Red`). Locking lights in certain states might be helpful in scenarios like E2E testing.
133
146
 
147
+ ![Admin Panel Screenshot](assets/admin.png)
148
+
134
149
  To add Admin Panel protected by basic authentication to your Rails project, add this configuration to your `config/routes.rb` file.
135
150
 
136
151
  ```ruby
@@ -519,7 +534,7 @@ class ApplicationController < ActionController::Base
519
534
 
520
535
  def stoplight(&block)
521
536
  Stoplight("#{params[:controller]}##{params[:action]}")
522
- .run(-> { render(nothing: true, status: :service_unavailable) }, &block)
537
+ .run(-> (*) { render(nothing: true, status: :service_unavailable) }, &block)
523
538
  end
524
539
  end
525
540
  ```
@@ -613,7 +628,7 @@ Example: "Ruby 3.2 reaches end-of-life in March 2026, so Stoplight 6.0 will requ
613
628
 
614
629
  After checking out the repo, run `bundle install` to install dependencies. Run tests with `bundle exec rspec` and check
615
630
  code style with `bundle exec standardrb`. We follow a git flow branching strategy - see our [Git Flow wiki page] for
616
- 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.
617
632
 
618
633
  ## Credits
619
634
 
@@ -12,7 +12,9 @@ require "redis"
12
12
  # "redis://admin:p4ssw0rd@10.0.1.1:6380/15"
13
13
  redis = Redis.new
14
14
  data_store = Stoplight::DataStore::Redis.new(redis)
15
+ error_notifier = Rails.error.method(:report)
15
16
 
16
17
  Stoplight.configure do |config|
17
18
  config.data_store = data_store
19
+ config.error_notifier = error_notifier
18
20
  end
@@ -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
@@ -8,7 +8,7 @@ module Stoplight
8
8
  attr_reader :data_store
9
9
  private :data_store
10
10
 
11
- # @param data_store [Stoplight::DataStore::Base]
11
+ # @param data_store [Stoplight::Domain::DataStore]
12
12
  def initialize(data_store:)
13
13
  @data_store = data_store
14
14
  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
@@ -15,11 +15,16 @@ module Stoplight
15
15
  end
16
16
 
17
17
  private def data_store
18
- settings.data_store.tap do |data_store|
19
- if data_store.is_a?(Stoplight::DataStore::Memory)
20
- raise "Stoplight Admin requires a persistent data store, but the current data store is Memory. " \
21
- "Please configure a different data store in your Stoplight configuration."
22
- end
18
+ if settings.data_store.is_a?(Stoplight::DataStore::Memory)
19
+ raise "Stoplight Admin requires a persistent data store, but the current data store is Memory. " \
20
+ "Please configure a different data store in your Stoplight configuration."
21
+ else
22
+ Stoplight::Wiring::LightBuilder.new(
23
+ {
24
+ data_store: settings.data_store,
25
+ config: Wiring::Light::DefaultConfig
26
+ }
27
+ ).__send__(:data_store)
23
28
  end
24
29
  end
25
30
  end
@@ -4,11 +4,11 @@ module Stoplight
4
4
  class Admin
5
5
  class LightsRepository
6
6
  # @!attribute data_store
7
- # @return [Stoplight::DataStore::Base]
7
+ # @return [Stoplight::Domain::DataStore]
8
8
  attr_reader :data_store
9
9
  private :data_store
10
10
 
11
- # @param data_store [Stoplight::DataStore::Base]
11
+ # @param data_store [Stoplight::Domain::DataStore]
12
12
  def initialize(data_store:)
13
13
  @data_store = data_store
14
14
  end
@@ -37,37 +37,49 @@ module Stoplight
37
37
  # color
38
38
  # @return [void]
39
39
  def lock(name, color = nil)
40
- light = build_light(name)
40
+ config = build_config(name)
41
+ color ||= data_store.get_state_snapshot(config).color
41
42
 
42
- case color || light.color
43
+ case color
43
44
  when Stoplight::Color::GREEN
44
- light.lock(Stoplight::Color::GREEN)
45
+ data_store.set_state(config, Stoplight::State::LOCKED_GREEN)
45
46
  else
46
- light.lock(Stoplight::Color::RED)
47
+ data_store.set_state(config, Stoplight::State::LOCKED_RED)
47
48
  end
48
49
  end
49
50
 
50
51
  # @param name [String] unlocks light by its name
51
52
  # @return [void]
52
53
  def unlock(name)
53
- build_light(name).unlock
54
+ config = build_config(name)
55
+ data_store.set_state(config, Domain::State::UNLOCKED)
56
+ end
57
+
58
+ # @param name [String] removes light metadata by its name
59
+ # @return [void]
60
+ def remove(name)
61
+ config = build_config(name)
62
+
63
+ data_store.delete_light(config)
54
64
  end
55
65
 
56
66
  private def load_light(name)
57
- light = build_light(name)
67
+ config = build_config(name)
68
+
58
69
  # failures, state
59
- metadata = data_store.get_metadata(light.config)
70
+ state_snapshot = data_store.get_state_snapshot(config)
71
+ metrics = data_store.get_metrics(config)
60
72
 
61
73
  Light.new(
62
74
  name: name,
63
- color: light.color,
64
- state: metadata.locked_state,
65
- failures: [metadata.last_error].compact
75
+ color: state_snapshot.color,
76
+ state: state_snapshot.locked_state,
77
+ failures: [metrics.last_error].compact
66
78
  )
67
79
  end
68
80
 
69
- private def build_light(name)
70
- Stoplight(name, data_store: data_store)
81
+ private def build_config(name)
82
+ Wiring::Light::DefaultConfig.with(name:)
71
83
  end
72
84
  end
73
85
  end
@@ -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,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi/escape"
4
+ require "cgi/util" if RUBY_VERSION < "3.5"
5
+
3
6
  begin
4
7
  require "sinatra/base"
5
- require "sinatra/contrib"
6
8
  require "sinatra/json"
7
9
  rescue LoadError
8
10
  raise <<~WARN
9
- "sinatra" and "sinatra-contrib" gems are unavailable and necessery for running Stoplight Admin panel
11
+ "sinatra" and "sinatra-contrib" gems are unavailable and necessary for running Stoplight Admin panel
10
12
  Please add them to your Gemfile and run `bundle install`:
11
13
  gem "sinatra", required: false
12
14
  gem "sinatra-contrib", require: false
@@ -25,13 +27,14 @@ module Stoplight
25
27
  helpers Helpers
26
28
 
27
29
  set :protection, except: %i[json_csrf]
28
- set :data_store, proc { Stoplight.default_config.data_store }
30
+ set :data_store, proc { Stoplight.__stoplight__default_configuration.data_store }
29
31
  set :views, File.join(__dir__, "admin", "views")
32
+ set :nonce, proc { |request| }
30
33
 
31
34
  get "/" do
32
35
  lights, stats = dependencies.stats_action.call
33
36
 
34
- erb :index, locals: stats.merge(lights: lights)
37
+ erb :index, locals: stats.merge(lights: lights, nonce: settings.nonce(request))
35
38
  end
36
39
 
37
40
  get "/stats" do
@@ -63,5 +66,11 @@ module Stoplight
63
66
 
64
67
  redirect to("/")
65
68
  end
69
+
70
+ post "/remove" do
71
+ dependencies.remove_action.call(params)
72
+
73
+ redirect to("/")
74
+ end
66
75
  end
67
76
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stoplight
4
+ module Common
5
+ module Deprecations
6
+ extend self
7
+
8
+ def deprecate(message) = warn("[DEPRECATION] #{message}")
9
+ end
10
+ end
11
+ 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,59 @@
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 - cool-off time in seconds
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
+
54
+ def cool_off_time_in_milliseconds
55
+ cool_off_time * 1_000
56
+ end
57
+ end
58
+ end
59
+ 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,56 @@ 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_metrics(config)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def clear_recovery_metrics(config)
21
52
  raise NotImplementedError
22
53
  end
23
54
 
24
55
  # Records a failure for a specific light configuration.
25
56
  #
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)
57
+ # @param config [Stoplight::Domain::Config]
58
+ # @param exception [Exception]
59
+ # @return [void]
60
+ def record_failure(config, exception)
30
61
  raise NotImplementedError
31
62
  end
32
63
 
33
64
  # Records a success for a specific light configuration.
34
65
  #
35
- # @param config [Stoplight::Light::Config]
66
+ # @param config [Stoplight::Domain::Config]
36
67
  # @return [void]
37
68
  def record_success(config)
38
69
  raise NotImplementedError
@@ -40,30 +71,42 @@ module Stoplight
40
71
 
41
72
  # Records a failed recovery probe for a specific light configuration.
42
73
  #
43
- # @param config [Stoplight::Light::Config]
74
+ # @param config [Stoplight::Domain::Config]
44
75
  # @param failure [Failure]
45
- # @return [Stoplight::Metadata]
76
+ # @return [void]
46
77
  def record_recovery_probe_failure(config, failure)
47
78
  raise NotImplementedError
48
79
  end
49
80
 
50
81
  # Records a successful recovery probe for a specific light configuration.
51
82
  #
52
- # @param config [Stoplight::Light::Config]
53
- # @return [Stoplight::Metadata]
83
+ # @param config [Stoplight::Domain::Config]
84
+ # @return [void]
54
85
  def record_recovery_probe_success(config)
55
86
  raise NotImplementedError
56
87
  end
57
88
 
58
89
  # Sets the state of a specific light configuration.
59
90
  #
60
- # @param config [Stoplight::Light::Config]
91
+ # @param config [Stoplight::Domain::Config]
61
92
  # @param state [String] The new state to set.
62
93
  # @return [String] The state that was set.
63
94
  def set_state(config, state)
64
95
  raise NotImplementedError
65
96
  end
66
97
 
98
+ # Acquires recovery lock for serializing probe execution.
99
+ #
100
+ # @param config [Stoplight::Domain::Config]
101
+ # @return [Stoplight::Domain::LockToken, nil] Lock if acquired, nil if contended
102
+ def acquire_recovery_lock(config) = raise NotImplementedError
103
+
104
+ # Releases previously acquired lock.
105
+ #
106
+ # @param lock [Stoplight::Domain::RecoveryLockToken]
107
+ # @return [void]
108
+ def release_recovery_lock(lock) = raise NotImplementedError
109
+
67
110
  # Transitions the Stoplight to the specified color.
68
111
  #
69
112
  # This method performs a color transition operation that works across distributed instances
@@ -71,7 +114,7 @@ module Stoplight
71
114
  # is considered the "first" to perform the transition (and therefore responsible for
72
115
  # triggering notifications).
73
116
  #
74
- # @param config [Stoplight::Light::Config]
117
+ # @param config [Stoplight::Domain::Config]
75
118
  # @param color [String] The target color/state to transition to.
76
119
  # Should be one of Stoplight::Color::GREEN, Stoplight::Color::YELLOW, or Stoplight::Color::RED.
77
120
  #
@@ -87,6 +130,17 @@ module Stoplight
87
130
  def transition_to_color(config, color)
88
131
  raise NotImplementedError
89
132
  end
133
+
134
+ # Deletes metadata (and related persistent state) for the given light.
135
+ #
136
+ # Implementations may choose to only remove metadata; metrics may expire via TTL.
137
+ #
138
+ # @param config [Stoplight::Domain::Config]
139
+ # @return [void]
140
+ def delete_light(config)
141
+ raise NotImplementedError
142
+ end
90
143
  end
144
+ # :nocov:
91
145
  end
92
146
  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