Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,6 @@ target :lib do
ignore "lib/stoplight/rspec"
ignore "lib/stoplight/rspec.rb"

ignore "lib/stoplight/wiring/data_store"
ignore "lib/stoplight/wiring/default.rb"
ignore "lib/stoplight/wiring/default_factory_builder.rb"
ignore "lib/stoplight/wiring/light"
ignore "lib/stoplight/wiring/light_builder.rb"
ignore "lib/stoplight/wiring/light_factory"
ignore "lib/stoplight/wiring/light_factory.rb"
ignore "lib/stoplight/wiring/public_api.rb"
ignore "lib/stoplight/wiring/system/light_factory.rb"
ignore "lib/stoplight/wiring/system.rb"

ignore "lib/stoplight.rb"

# library "pathname" # Standard libraries
# library "strong_json" # Gems

Expand Down
153 changes: 122 additions & 31 deletions lib/stoplight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "concurrent/map"
require "zeitwerk"

# steep:ignore:start
loader = Zeitwerk::Loader.for_gem
loader.inflector.inflect("io" => "IO")
loader.do_not_eager_load(
Expand All @@ -13,6 +14,7 @@
loader.ignore("#{__dir__}/generators")
loader.ignore("#{__dir__}/stoplight/rspec.rb", "#{__dir__}/stoplight/rspec")
loader.setup
# steep:ignore:end

module Stoplight # rubocop:disable Style/Documentation
include Wiring::PublicApi
Expand Down Expand Up @@ -66,24 +68,69 @@ def configure(trust_me_im_an_engineer: false)
end
end

# Creates a Light for internal use.
#
# @param name [String]
# @param settings [Hash]
# @return [Stoplight::Light]
# @api private
def system_light(name, **settings)
Wiring::LightFactory.new.with(name: "__stoplight__#{name}", **settings).build
def system_light(
name,
cool_off_time: T.undefined,
threshold: T.undefined,
recovery_threshold: T.undefined,
window_size: T.undefined,
tracked_errors: T.undefined,
skipped_errors: T.undefined,
data_store: T.undefined,
error_notifier: T.undefined,
notifiers: T.undefined,
traffic_control: T.undefined,
traffic_recovery: T.undefined
)
Wiring::LightFactory.new(settings: Wiring::Settings.empty).build_with(
name: "__stoplight__#{name}",
cool_off_time:,
threshold:,
recovery_threshold:,
window_size:,
tracked_errors:,
skipped_errors:,
data_store:,
error_notifier:,
notifiers:,
traffic_control:,
traffic_recovery:
)
end

# Create a Light with the user default configuration.
#
# @param name [String]
# @param settings [Hash]
# @return [Stoplight::Light]
# @api private
def light(name, **settings)
__stoplight__default_light_factory.build_with(name:, **settings)
def light(
name,
cool_off_time: T.undefined,
threshold: T.undefined,
recovery_threshold: T.undefined,
window_size: T.undefined,
tracked_errors: T.undefined,
skipped_errors: T.undefined,
data_store: T.undefined,
error_notifier: T.undefined,
notifiers: T.undefined,
traffic_control: T.undefined,
traffic_recovery: T.undefined
)
__stoplight__default_light_factory.build_with(
name:,
cool_off_time:,
threshold:,
recovery_threshold:,
window_size:,
tracked_errors:,
skipped_errors:,
data_store:,
error_notifier:,
notifiers:,
traffic_control:,
traffic_recovery:
)
end

# Creates a new named system with the given configuration.
Expand All @@ -110,30 +157,48 @@ def light(name, **settings)
# Analytics = Stoplight.__stoplight__system(:analytics, data_store: analytics_redis)
#
# @api private
def __stoplight__system(name, **settings)
def __stoplight__system(
name,
cool_off_time: T.undefined,
threshold: T.undefined,
recovery_threshold: T.undefined,
window_size: T.undefined,
tracked_errors: T.undefined,
skipped_errors: T.undefined,
data_store: T.undefined,
error_notifier: T.undefined,
notifiers: T.undefined,
traffic_control: T.undefined,
traffic_recovery: T.undefined
)
ensure_configured

@systems.compute(name.to_s) do |existing_system|
settings = default_configuration.to_settings.extend_with(
cool_off_time: cool_off_time,
threshold: threshold,
recovery_threshold: recovery_threshold,
window_size: window_size,
tracked_errors: tracked_errors,
skipped_errors: skipped_errors,
traffic_control: traffic_control,
traffic_recovery: traffic_recovery,
data_store: data_store,
error_notifier: error_notifier,
notifiers: notifiers
)

systems.compute(name.to_s) do |existing_system|
if existing_system
raise ArgumentError, "system `#{name}` is already in use"
else
Stoplight::Wiring::System.new(name.to_s, **{
cool_off_time: @default_configuration.cool_off_time,
threshold: @default_configuration.threshold,
recovery_threshold: @default_configuration.recovery_threshold,
window_size: @default_configuration.window_size,
tracked_errors: @default_configuration.tracked_errors,
skipped_errors: @default_configuration.skipped_errors,
data_store: @default_configuration.data_store,
error_notifier: @default_configuration.error_notifier,
notifiers: @default_configuration.notifiers,
traffic_control: @default_configuration.traffic_control,
traffic_recovery: @default_configuration.traffic_recovery
}.compact.merge(settings))
Stoplight::Wiring::System.new(name.to_s, settings:)
end
end
end

private attr_reader :systems
private def default_configuration = T.must(@default_configuration)

# Resets Stoplight to an unconfigured state.
#
# Clears all registered systems, default configuration, and the default light factory.
Expand Down Expand Up @@ -170,12 +235,12 @@ def __stoplight__reset!
# @api private
def __stoplight__default_light_factory
ensure_configured
@default_light_factory
T.must(@default_light_factory)
end

def __stoplight__default_configuration
ensure_configured
@default_configuration
T.must(@default_configuration)
end

# @api private
Expand Down Expand Up @@ -243,8 +308,21 @@ def __stoplight__default_configuration
# # When 66.6% error rate reached withing a sliding 5 minute window, the circuit breaker will trip.
# light = Stoplight("Payment API", traffic_control: :error_rate, threshold: 0.666, window_size: 300)
#
def Stoplight(name, **settings) # rubocop:disable Naming/MethodName
Stoplight::Common::Deprecations.deprecate(<<~MSG) if settings.include?(:error_notifier)
def Stoplight(
name,
cool_off_time: Stoplight::T.undefined,
threshold: Stoplight::T.undefined,
recovery_threshold: Stoplight::T.undefined,
window_size: Stoplight::T.undefined,
tracked_errors: Stoplight::T.undefined,
skipped_errors: Stoplight::T.undefined,
data_store: Stoplight::T.undefined,
error_notifier: Stoplight::T.undefined,
notifiers: Stoplight::T.undefined,
traffic_control: Stoplight::T.undefined,
traffic_recovery: Stoplight::T.undefined
) # rubocop:disable Naming/MethodName
Stoplight::Common::Deprecations.deprecate(<<~MSG) if error_notifier != Stoplight::T.undefined
Passing "error_notifier" to Stoplight('#{name}') is deprecated and will be removed in v6.0.0.

IMPORTANT: The `error_notifier` is NOT called for exceptions in your protected code.
Expand All @@ -258,5 +336,18 @@ def Stoplight(name, **settings) # rubocop:disable Naming/MethodName

See: https://github.com/bolshakov/stoplight#error-notifiers
MSG
Stoplight.light(name, **settings)
Stoplight.light(
name,
cool_off_time:,
threshold:,
recovery_threshold:,
window_size:,
tracked_errors:,
skipped_errors:,
data_store:,
error_notifier:,
notifiers:,
traffic_control:,
traffic_recovery:
)
end
8 changes: 4 additions & 4 deletions lib/stoplight/admin/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def dependencies
"Please configure a different data store in your Stoplight configuration."
else
Stoplight::Wiring::LightBuilder.new(
{
data_store: settings.data_store,
config: Wiring::Light::DefaultConfig
}
config: Wiring::Light::DefaultConfig.with(
data_store: settings.data_store
),
factory: nil
).__send__(:data_store)
end
end
Expand Down
14 changes: 14 additions & 0 deletions lib/stoplight/common.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Stoplight
# Shared utilities and data structures.
module Common
# Wraps a value in a Some, indicating presence.
def self.some(value)
Some.new(value) # steep:ignore
end

# Returns a None instance, indicating absence.W
def self.none
None.new
end
end
end
29 changes: 29 additions & 0 deletions lib/stoplight/common/none.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Stoplight
module Common
# Represents the absence of a value in an Option type.
#
# When a setting is None, it means the user hasn't configured it,
# allowing downstream code to apply library defaults.
#
# @see Some
class None
def get_or_else
yield
end

def map
self
end

def ==(other)
other.is_a?(None)
end

def to_s
"None"
end

def empty? = true
end
end
end
36 changes: 36 additions & 0 deletions lib/stoplight/common/some.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Stoplight
module Common
# Represents the presence of a value in an Option type.
#
# Used to distinguish between "explicitly configured" (Some) and
# "not configured" (None) in the Settings DSL, enabling three-state
# logic: configured, not-configured, and explicitly-nil.
#
# @see None
class Some
protected attr_reader :value

def initialize(value)
@value = value
end

def get_or_else
@value
end

def map
Some.new(yield value)
end

def ==(other)
other.is_a?(Some) && value == other.value # steep:ignore
end

def to_s
"Some(#{@value})"
end

def empty? = false
end
end
end
9 changes: 7 additions & 2 deletions lib/stoplight/domain/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ module Domain
:recovery_threshold,
:window_size,
:tracked_errors,
:skipped_errors
:skipped_errors,
:traffic_control,
:traffic_recovery,
:error_notifier,
:notifiers,
:data_store
)
class Config
# # @!attribute [r] name
Expand Down Expand Up @@ -45,7 +50,7 @@ def track_error?(error)
end

def cool_off_time_in_milliseconds
cool_off_time * 1_000
(cool_off_time * 1_000).to_i
end
end
end
Expand Down
Loading