stoplight 5.6.0 → 5.8.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/UPGRADING.md +303 -0
- data/lib/generators/stoplight/install/install_generator.rb +6 -1
- data/lib/stoplight/admin/dependencies.rb +1 -1
- data/lib/stoplight/admin/helpers.rb +26 -5
- data/lib/stoplight/admin/lights_repository/light.rb +22 -6
- data/lib/stoplight/admin/lights_repository.rb +20 -16
- data/lib/stoplight/admin/views/_card.erb +8 -5
- data/lib/stoplight/admin.rb +2 -1
- data/lib/stoplight/color.rb +9 -0
- data/lib/stoplight/common/deprecations.rb +11 -0
- data/lib/stoplight/data_store.rb +28 -0
- data/lib/stoplight/domain/compatibility_result.rb +7 -7
- data/lib/stoplight/domain/config.rb +38 -35
- data/lib/stoplight/domain/error_tracking_policy.rb +27 -0
- data/lib/stoplight/domain/failure.rb +1 -1
- data/lib/stoplight/domain/light/configuration_builder_interface.rb +122 -16
- data/lib/stoplight/domain/light.rb +44 -64
- data/lib/stoplight/domain/light_info.rb +7 -0
- data/lib/stoplight/domain/metrics_snapshot.rb +58 -0
- data/lib/stoplight/domain/state_snapshot.rb +29 -23
- data/lib/stoplight/domain/storage/recovery_lock_token.rb +15 -0
- data/lib/stoplight/domain/strategies/green_run_strategy.rb +18 -26
- data/lib/stoplight/domain/strategies/red_run_strategy.rb +9 -12
- data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +74 -58
- data/lib/stoplight/domain/tracker/recovery_probe.rb +27 -43
- data/lib/stoplight/domain/tracker/request.rb +24 -39
- data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +8 -11
- data/lib/stoplight/domain/traffic_control/error_rate.rb +19 -15
- data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +8 -18
- data/lib/stoplight/domain/traffic_recovery.rb +3 -5
- data/lib/stoplight/error.rb +46 -0
- data/lib/stoplight/infrastructure/fail_safe/data_store.rb +152 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/metrics.rb +65 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock.rb +69 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rb +19 -0
- data/lib/stoplight/infrastructure/fail_safe/storage/state.rb +62 -0
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/metrics.rb +2 -2
- data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_store.rb +52 -0
- data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_token.rb +17 -0
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/sliding_window.rb +21 -26
- data/lib/stoplight/infrastructure/{data_store/memory → memory/data_store}/state.rb +3 -3
- data/lib/stoplight/infrastructure/{data_store/memory.rb → memory/data_store.rb} +90 -57
- data/lib/stoplight/infrastructure/memory/storage/recovery_lock.rb +35 -0
- data/lib/stoplight/infrastructure/memory/storage/recovery_metrics.rb +16 -0
- data/lib/stoplight/infrastructure/memory/storage/state.rb +155 -0
- data/lib/stoplight/infrastructure/memory/storage/unbounded_metrics.rb +103 -0
- data/lib/stoplight/infrastructure/memory/storage/window_metrics.rb +101 -0
- data/lib/stoplight/infrastructure/notifier/fail_safe.rb +50 -0
- data/lib/stoplight/infrastructure/notifier/generic.rb +4 -14
- data/lib/stoplight/infrastructure/notifier/io.rb +1 -2
- data/lib/stoplight/infrastructure/notifier/logger.rb +1 -2
- data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_failure.lua +27 -0
- data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_success.lua +23 -0
- data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_store.rb +60 -0
- data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_token.rb +28 -0
- data/lib/stoplight/infrastructure/redis/data_store/scripting.rb +73 -0
- data/lib/stoplight/infrastructure/{data_store/redis.rb → redis/data_store.rb} +173 -210
- data/lib/stoplight/infrastructure/redis/storage/key_space.rb +51 -0
- data/lib/stoplight/infrastructure/redis/storage/metrics.rb +40 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_lock/release_lock.lua +6 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_lock.rb +64 -0
- data/lib/stoplight/infrastructure/redis/storage/recovery_metrics.rb +20 -0
- data/lib/stoplight/infrastructure/redis/storage/scripting.rb +18 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua +10 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +10 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua +9 -0
- data/lib/stoplight/infrastructure/redis/storage/state.rb +141 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_failure.lua +28 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_success.lua +26 -0
- data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb +123 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/metrics_snapshot.lua +26 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_failure.lua +36 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_success.lua +35 -0
- data/lib/stoplight/infrastructure/redis/storage/window_metrics.rb +174 -0
- data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +41 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +33 -0
- data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +47 -0
- data/lib/stoplight/infrastructure/storage/compatibility_state.rb +44 -0
- data/lib/stoplight/infrastructure/system_clock.rb +16 -0
- data/lib/stoplight/notifier.rb +11 -0
- data/lib/stoplight/state.rb +9 -0
- data/lib/stoplight/types.rb +29 -0
- data/lib/stoplight/undefined.rb +16 -0
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/config_compatibility_validator.rb +54 -0
- data/lib/stoplight/wiring/configuration_dsl.rb +101 -0
- data/lib/stoplight/wiring/data_store_backend.rb +26 -0
- data/lib/stoplight/wiring/default.rb +2 -2
- data/lib/stoplight/wiring/default_config.rb +21 -0
- data/lib/stoplight/wiring/default_configuration.rb +70 -53
- data/lib/stoplight/wiring/light_builder.rb +198 -0
- data/lib/stoplight/wiring/light_factory/traffic_control_dsl.rb +26 -0
- data/lib/stoplight/wiring/light_factory/traffic_recovery_dsl.rb +21 -0
- data/lib/stoplight/wiring/light_factory.rb +74 -135
- data/lib/stoplight/wiring/memory/backend.rb +57 -0
- data/lib/stoplight/wiring/notifier_factory.rb +26 -0
- data/lib/stoplight/wiring/redis/backend.rb +116 -0
- data/lib/stoplight/wiring/storage_set.rb +12 -0
- data/lib/stoplight/wiring/storage_set_builder.rb +51 -0
- data/lib/stoplight/wiring/system/light_builder.rb +47 -0
- data/lib/stoplight/wiring/system/light_factory.rb +64 -0
- data/lib/stoplight/wiring/system.rb +129 -0
- data/lib/stoplight.rb +209 -23
- data/sig/_private/generators/stoplight/install/install_generator.rbs +22 -0
- data/sig/_private/stoplight/common/deprecations.rbs +9 -0
- data/sig/_private/stoplight/data_store.rbs +6 -0
- data/sig/_private/stoplight/domain/compatibility_result.rbs +18 -0
- data/sig/_private/stoplight/domain/config.rbs +65 -0
- data/sig/_private/stoplight/domain/error_tracking_policy.rbs +14 -0
- data/sig/_private/stoplight/domain/failure.rbs +16 -0
- data/sig/_private/stoplight/domain/light.rbs +25 -0
- data/sig/_private/stoplight/domain/light_info.rbs +19 -0
- data/sig/_private/stoplight/domain/metrics_snapshot.rbs +38 -0
- data/sig/_private/stoplight/domain/ports/clock.rbs +18 -0
- data/sig/_private/stoplight/domain/ports/data_store.rbs +76 -0
- data/{lib/stoplight/domain/light_factory.rb → sig/_private/stoplight/domain/ports/light_factory.rbs} +33 -28
- data/sig/_private/stoplight/domain/ports/metrics_store.rbs +29 -0
- data/sig/_private/stoplight/domain/ports/recovery_lock_store.rbs +52 -0
- data/sig/_private/stoplight/domain/ports/recovery_lock_token.rbs +6 -0
- data/sig/_private/stoplight/domain/ports/run_strategy.rbs +14 -0
- data/sig/_private/stoplight/domain/ports/state_store.rbs +79 -0
- data/sig/_private/stoplight/domain/ports/traffic_control.rbs +41 -0
- data/sig/_private/stoplight/domain/ports/traffic_recovery.rbs +47 -0
- data/sig/_private/stoplight/domain/state_snapshot.rbs +32 -0
- data/sig/_private/stoplight/domain/storage/recovery_lock_token.rbs +11 -0
- data/sig/_private/stoplight/domain/strategies/green_run_strategy.rbs +17 -0
- data/sig/_private/stoplight/domain/strategies/red_run_strategy.rbs +17 -0
- data/sig/_private/stoplight/domain/strategies/yellow_run_strategy.rbs +42 -0
- data/sig/_private/stoplight/domain/tracker/base.rbs +8 -0
- data/sig/_private/stoplight/domain/tracker/recovery_probe.rbs +25 -0
- data/sig/_private/stoplight/domain/tracker/request.rbs +26 -0
- data/sig/_private/stoplight/domain/traffic_control/consecutive_errors.rbs +9 -0
- data/sig/_private/stoplight/domain/traffic_control/error_rate.rbs +13 -0
- data/sig/_private/stoplight/domain/traffic_recovery/consecutive_successes.rbs +9 -0
- data/sig/_private/stoplight/domain/traffic_recovery.rbs +9 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/data_store.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock.rbs +29 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/recovery_lock_token.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/fail_safe/storage/state.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_store.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_token.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/sliding_window.rbs +27 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store/state.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/memory/data_store.rbs +30 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/recovery_lock.rbs +15 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/recovery_metrics.rbs +10 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/state.rbs +28 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/unbounded_metrics.rbs +25 -0
- data/sig/_private/stoplight/infrastructure/memory/storage/window_metrics.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/notifier/fail_safe.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/notifier/generic.rbs +18 -0
- data/sig/_private/stoplight/infrastructure/notifier/io.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/notifier/logger.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_store.rbs +24 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_token.rbs +21 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store/scripting.rbs +34 -0
- data/sig/_private/stoplight/infrastructure/redis/data_store.rbs +67 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/key_space.rbs +19 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/metrics.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/recovery_lock.rbs +26 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/recovery_metrics.rbs +10 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/scripting.rbs +13 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/state.rbs +32 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/unbounded_metrics.rbs +21 -0
- data/sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs +34 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_metrics.rbs +17 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_lock.rbs +13 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_metrics.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/storage/compatibility_state.rbs +14 -0
- data/sig/_private/stoplight/infrastructure/system_clock.rbs +7 -0
- data/sig/_private/stoplight/system/light_builder.rbs +23 -0
- data/sig/_private/stoplight/system/light_factory.rbs +17 -0
- data/sig/_private/stoplight/types.rbs +6 -0
- data/sig/_private/stoplight/wiring/config_compatibility_validator.rbs +19 -0
- data/sig/_private/stoplight/wiring/configuration_dsl.rbs +43 -0
- data/sig/_private/stoplight/wiring/data_store_backend.rbs +11 -0
- data/sig/_private/stoplight/wiring/default.rbs +26 -0
- data/sig/_private/stoplight/wiring/default_config.rbs +7 -0
- data/sig/_private/stoplight/wiring/default_configuration.rbs +29 -0
- data/sig/_private/stoplight/wiring/light_builder.rbs +48 -0
- data/sig/_private/stoplight/wiring/light_factory/traffic_control_dsl.rbs +7 -0
- data/sig/_private/stoplight/wiring/light_factory/traffic_recovery_dsl.rbs +7 -0
- data/sig/_private/stoplight/wiring/light_factory.rbs +16 -0
- data/sig/_private/stoplight/wiring/memory/backend.rbs +26 -0
- data/sig/_private/stoplight/wiring/notifier_factory.rbs +10 -0
- data/sig/_private/stoplight/wiring/redis/backend.rbs +38 -0
- data/sig/_private/stoplight/wiring/storage_set.rbs +38 -0
- data/sig/_private/stoplight/wiring/storage_set_builder.rbs +15 -0
- data/sig/_private/stoplight/wiring/system.rbs +15 -0
- data/sig/_private/stoplight.rbs +48 -0
- data/sig/stoplight/color.rbs +7 -0
- data/sig/stoplight/data_store.rbs +19 -0
- data/sig/stoplight/error.rbs +20 -0
- data/sig/stoplight/notifier.rbs +11 -0
- data/sig/stoplight/ports/configuration.rbs +19 -0
- data/sig/stoplight/ports/exception_matcher.rbs +8 -0
- data/sig/stoplight/ports/light.rbs +12 -0
- data/sig/stoplight/ports/light_info.rbs +5 -0
- data/sig/stoplight/ports/state_transition_notifier.rbs +15 -0
- data/sig/stoplight/ports/system.rbs +21 -0
- data/sig/stoplight/state.rbs +7 -0
- data/sig/stoplight/undefined.rbs +9 -0
- data/sig/stoplight/version.rbs +3 -0
- data/sig/stoplight.rbs +66 -0
- metadata +199 -36
- data/lib/stoplight/domain/color.rb +0 -11
- data/lib/stoplight/domain/data_store.rb +0 -130
- data/lib/stoplight/domain/error.rb +0 -42
- data/lib/stoplight/domain/metrics.rb +0 -85
- data/lib/stoplight/domain/state.rb +0 -11
- data/lib/stoplight/domain/state_transition_notifier.rb +0 -25
- data/lib/stoplight/domain/strategies/run_strategy.rb +0 -27
- data/lib/stoplight/domain/tracker/base.rb +0 -41
- data/lib/stoplight/domain/traffic_control/base.rb +0 -74
- data/lib/stoplight/domain/traffic_recovery/base.rb +0 -80
- data/lib/stoplight/infrastructure/data_store/redis/lua.rb +0 -25
- data/lib/stoplight/infrastructure/dependency_injection/container.rb +0 -249
- data/lib/stoplight/infrastructure/dependency_injection/unresolved_dependency_error.rb +0 -13
- data/lib/stoplight/wiring/container.rb +0 -80
- data/lib/stoplight/wiring/default_factory_builder.rb +0 -25
- data/lib/stoplight/wiring/fail_safe_data_store.rb +0 -147
- data/lib/stoplight/wiring/fail_safe_notifier.rb +0 -79
- data/lib/stoplight/wiring/light/default_config.rb +0 -18
- data/lib/stoplight/wiring/light/system_config.rb +0 -11
- data/lib/stoplight/wiring/public_api.rb +0 -28
- data/lib/stoplight/wiring/system_container.rb +0 -9
- data/lib/stoplight/wiring/system_light_factory.rb +0 -17
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/get_metrics.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/record_failure.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/record_success.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_green.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_red.lua +0 -0
- /data/lib/stoplight/infrastructure/{data_store/redis → redis/data_store/lua_scripts}/transition_to_yellow.lua +0 -0
|
@@ -9,28 +9,28 @@ module Stoplight
|
|
|
9
9
|
class << self
|
|
10
10
|
# Creates a new +CompatibilityResult+ instance representing a compatible strategy.
|
|
11
11
|
#
|
|
12
|
-
# @return
|
|
12
|
+
# @return An instance with no errors.
|
|
13
13
|
def compatible
|
|
14
14
|
new(errors: [])
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Creates a new +CompatibilityResult+ instance representing an incompatible strategy.
|
|
18
18
|
#
|
|
19
|
-
# @param errors
|
|
20
|
-
# @return
|
|
19
|
+
# @param errors List of error messages indicating incompatibility.
|
|
20
|
+
# @return An instance with the provided errors.
|
|
21
21
|
def incompatible(*errors)
|
|
22
22
|
new(errors:)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Initializes a new `CompatibilityResult` instance.
|
|
27
|
-
# @param errors
|
|
27
|
+
# @param errors List of error messages if the strategy is not compatible.
|
|
28
28
|
def initialize(errors: [])
|
|
29
29
|
@errors = errors.freeze
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Checks if the strategy is compatible.
|
|
33
|
-
# @return
|
|
33
|
+
# @return `true` if there are no errors, `false` otherwise.
|
|
34
34
|
def compatible?
|
|
35
35
|
@errors.empty?
|
|
36
36
|
end
|
|
@@ -38,11 +38,11 @@ module Stoplight
|
|
|
38
38
|
def incompatible? = !compatible?
|
|
39
39
|
|
|
40
40
|
# Retrieves the list of error messages.
|
|
41
|
-
# @return
|
|
41
|
+
# @return The list of error messages.
|
|
42
42
|
attr_reader :errors
|
|
43
43
|
|
|
44
44
|
# Retrieves a concatenated error message string.
|
|
45
|
-
# @return
|
|
45
|
+
# @return A string containing all error messages joined by "; ",
|
|
46
46
|
# or `nil` if the strategy is compatible.
|
|
47
47
|
def error_messages
|
|
48
48
|
unless compatible?
|
|
@@ -4,24 +4,6 @@ module Stoplight
|
|
|
4
4
|
module Domain
|
|
5
5
|
# A +Stoplight::Light+ configuration object.
|
|
6
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
7
|
# @api private
|
|
26
8
|
Config = Data.define(
|
|
27
9
|
:name,
|
|
@@ -30,25 +12,46 @@ module Stoplight
|
|
|
30
12
|
:recovery_threshold,
|
|
31
13
|
:window_size,
|
|
32
14
|
:tracked_errors,
|
|
33
|
-
:skipped_errors
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
:skipped_errors,
|
|
16
|
+
:traffic_control,
|
|
17
|
+
:traffic_recovery,
|
|
18
|
+
:error_notifier,
|
|
19
|
+
:notifiers,
|
|
20
|
+
:data_store
|
|
21
|
+
)
|
|
22
|
+
class Config
|
|
23
|
+
def cool_off_time_in_milliseconds
|
|
24
|
+
(cool_off_time * 1_000).to_i
|
|
41
25
|
end
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
def with(
|
|
28
|
+
name: T.undefined,
|
|
29
|
+
cool_off_time: T.undefined,
|
|
30
|
+
threshold: T.undefined,
|
|
31
|
+
recovery_threshold: T.undefined,
|
|
32
|
+
window_size: T.undefined,
|
|
33
|
+
skipped_errors: T.undefined,
|
|
34
|
+
tracked_errors: T.undefined,
|
|
35
|
+
traffic_control: T.undefined,
|
|
36
|
+
traffic_recovery: T.undefined,
|
|
37
|
+
error_notifier: T.undefined,
|
|
38
|
+
notifiers: T.undefined,
|
|
39
|
+
data_store: T.undefined
|
|
40
|
+
)
|
|
41
|
+
super(
|
|
42
|
+
name: name.is_a?(Undefined) ? self.name : name,
|
|
43
|
+
cool_off_time: cool_off_time.is_a?(Undefined) ? self.cool_off_time : cool_off_time,
|
|
44
|
+
threshold: threshold.is_a?(Undefined) ? self.threshold : threshold,
|
|
45
|
+
recovery_threshold: recovery_threshold.is_a?(Undefined) ? self.recovery_threshold : recovery_threshold,
|
|
46
|
+
window_size: window_size.is_a?(Undefined) ? self.window_size : window_size,
|
|
47
|
+
skipped_errors: skipped_errors.is_a?(Undefined) ? self.skipped_errors : skipped_errors,
|
|
48
|
+
tracked_errors: tracked_errors.is_a?(Undefined) ? self.tracked_errors : tracked_errors,
|
|
49
|
+
traffic_control: traffic_control.is_a?(Undefined) ? self.traffic_control : traffic_control,
|
|
50
|
+
traffic_recovery: traffic_recovery.is_a?(Undefined) ? self.traffic_recovery : traffic_recovery,
|
|
51
|
+
error_notifier: error_notifier.is_a?(Undefined) ? self.error_notifier : error_notifier,
|
|
52
|
+
notifiers: notifiers.is_a?(Undefined) ? self.notifiers : notifiers,
|
|
53
|
+
data_store: data_store.is_a?(Undefined) ? self.data_store : data_store,
|
|
54
|
+
)
|
|
52
55
|
end
|
|
53
56
|
end
|
|
54
57
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
# Determines which errors should be traced
|
|
6
|
+
class ErrorTrackingPolicy
|
|
7
|
+
def initialize(tracked:, skipped:)
|
|
8
|
+
@tracked = tracked
|
|
9
|
+
@skipped = skipped
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def track?(error)
|
|
13
|
+
!skipped?(error) && tracked?(error)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def skipped?(error)
|
|
19
|
+
@skipped.any? { |matcher| matcher === error }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tracked?(error)
|
|
23
|
+
@tracked.any? { |matcher| matcher === error }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -6,6 +6,7 @@ module Stoplight
|
|
|
6
6
|
module Domain
|
|
7
7
|
class Light
|
|
8
8
|
# Implements light configuration behavior
|
|
9
|
+
# steep:ignore:start
|
|
9
10
|
module ConfigurationBuilderInterface
|
|
10
11
|
# Configures data store to be used with this circuit breaker
|
|
11
12
|
#
|
|
@@ -15,9 +16,22 @@ module Stoplight
|
|
|
15
16
|
#
|
|
16
17
|
# @param data_store [DataStore::Base]
|
|
17
18
|
# @return [Stoplight::Light]
|
|
18
|
-
# @deprecated
|
|
19
|
+
# @deprecated
|
|
19
20
|
def with_data_store(data_store)
|
|
20
|
-
|
|
21
|
+
deprecate(<<~MSG)
|
|
22
|
+
Light#with_data_store is deprecated and will be removed in v6.0.0.
|
|
23
|
+
|
|
24
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
25
|
+
modifications.
|
|
26
|
+
|
|
27
|
+
Instead of:
|
|
28
|
+
light = Stoplight('api-call')
|
|
29
|
+
modified = light.with_data_store(data_stare)
|
|
30
|
+
|
|
31
|
+
Configure correctly from the start:
|
|
32
|
+
Stoplight('api-call', data_store:)
|
|
33
|
+
MSG
|
|
34
|
+
with_without_warning(data_store:)
|
|
21
35
|
end
|
|
22
36
|
|
|
23
37
|
# Configures cool off time. Stoplight automatically tries to recover
|
|
@@ -29,9 +43,22 @@ module Stoplight
|
|
|
29
43
|
#
|
|
30
44
|
# @param cool_off_time [Numeric] number of seconds
|
|
31
45
|
# @return [Stoplight::Light]
|
|
32
|
-
# @deprecated
|
|
46
|
+
# @deprecated
|
|
33
47
|
def with_cool_off_time(cool_off_time)
|
|
34
|
-
|
|
48
|
+
deprecate(<<~MSG)
|
|
49
|
+
Light#with_cool_off_time is deprecated and will be removed in v6.0.0.
|
|
50
|
+
|
|
51
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
52
|
+
modifications.
|
|
53
|
+
|
|
54
|
+
Instead of:
|
|
55
|
+
light = Stoplight('api-call')
|
|
56
|
+
modified = light.with_cool_off_time(cool_off_time)
|
|
57
|
+
|
|
58
|
+
Configure correctly from the start:
|
|
59
|
+
Stoplight('api-call', cool_off_time:)
|
|
60
|
+
MSG
|
|
61
|
+
with_without_warning(cool_off_time:)
|
|
35
62
|
end
|
|
36
63
|
|
|
37
64
|
# Configures custom threshold. After this number of failures Stoplight
|
|
@@ -43,9 +70,22 @@ module Stoplight
|
|
|
43
70
|
#
|
|
44
71
|
# @param threshold [Numeric]
|
|
45
72
|
# @return [Stoplight::Light]
|
|
46
|
-
# @deprecated
|
|
73
|
+
# @deprecated
|
|
47
74
|
def with_threshold(threshold)
|
|
48
|
-
|
|
75
|
+
deprecate(<<~MSG)
|
|
76
|
+
Light#with_threshold is deprecated and will be removed in v6.0.0.
|
|
77
|
+
|
|
78
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
79
|
+
modifications.
|
|
80
|
+
|
|
81
|
+
Instead of:
|
|
82
|
+
light = Stoplight('api-call')
|
|
83
|
+
modified = light.with_threshold(threshold)
|
|
84
|
+
|
|
85
|
+
Configure correctly from the start:
|
|
86
|
+
Stoplight('api-call', threshold:)
|
|
87
|
+
MSG
|
|
88
|
+
with_without_warning(threshold:)
|
|
49
89
|
end
|
|
50
90
|
|
|
51
91
|
# Configures custom window size which Stoplight uses to count failures. For example,
|
|
@@ -60,9 +100,22 @@ module Stoplight
|
|
|
60
100
|
#
|
|
61
101
|
# @param window_size [Numeric] number of seconds
|
|
62
102
|
# @return [Stoplight::Light]
|
|
63
|
-
# @deprecated
|
|
103
|
+
# @deprecated
|
|
64
104
|
def with_window_size(window_size)
|
|
65
|
-
|
|
105
|
+
deprecate(<<~MSG)
|
|
106
|
+
Light#with_window_size is deprecated and will be removed in v6.0.0.
|
|
107
|
+
|
|
108
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
109
|
+
modifications.
|
|
110
|
+
|
|
111
|
+
Instead of:
|
|
112
|
+
light = Stoplight('api-call')
|
|
113
|
+
modified = light.with_window_size(window_size)
|
|
114
|
+
|
|
115
|
+
Configure correctly from the start:
|
|
116
|
+
Stoplight('api-call', window_size:)
|
|
117
|
+
MSG
|
|
118
|
+
with_without_warning(window_size:)
|
|
66
119
|
end
|
|
67
120
|
|
|
68
121
|
# Configures custom notifier
|
|
@@ -75,17 +128,43 @@ module Stoplight
|
|
|
75
128
|
#
|
|
76
129
|
# @param notifiers [Array<Notifier::Base>]
|
|
77
130
|
# @return [Stoplight::Light]
|
|
78
|
-
# @deprecated
|
|
131
|
+
# @deprecated
|
|
79
132
|
def with_notifiers(notifiers)
|
|
80
|
-
|
|
133
|
+
deprecate(<<~MSG)
|
|
134
|
+
Light#with_notifiers is deprecated and will be removed in v6.0.0.
|
|
135
|
+
|
|
136
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
137
|
+
modifications.
|
|
138
|
+
|
|
139
|
+
Instead of:
|
|
140
|
+
light = Stoplight('api-call')
|
|
141
|
+
modified = light.with_notifiers(notifiers)
|
|
142
|
+
|
|
143
|
+
Configure correctly from the start:
|
|
144
|
+
Stoplight('api-call', notifiers:)
|
|
145
|
+
MSG
|
|
146
|
+
with_without_warning(notifiers:)
|
|
81
147
|
end
|
|
82
148
|
|
|
83
149
|
# @param error_notifier [Proc]
|
|
84
150
|
# @return [Stoplight::Light]
|
|
85
151
|
# @api private
|
|
86
|
-
# @deprecated
|
|
152
|
+
# @deprecated
|
|
87
153
|
def with_error_notifier(&error_notifier)
|
|
88
|
-
|
|
154
|
+
deprecate(<<~MSG)
|
|
155
|
+
Light#with_error_notifier is deprecated and will be removed in v6.0.0.
|
|
156
|
+
|
|
157
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
158
|
+
modifications.
|
|
159
|
+
|
|
160
|
+
Instead of:
|
|
161
|
+
light = Stoplight('api-call')
|
|
162
|
+
modified = light.with_error_notifier { |error| warn error }
|
|
163
|
+
|
|
164
|
+
Configure correctly from the start:
|
|
165
|
+
Stoplight('api-call', error_notifier: ->(error) { warn error })
|
|
166
|
+
MSG
|
|
167
|
+
with_without_warning(error_notifier: error_notifier)
|
|
89
168
|
end
|
|
90
169
|
|
|
91
170
|
# Configures a custom list of tracked errors that counts toward the threshold.
|
|
@@ -101,9 +180,22 @@ module Stoplight
|
|
|
101
180
|
#
|
|
102
181
|
# @param tracked_errors [Array<StandardError>]
|
|
103
182
|
# @return [Stoplight::Light]
|
|
104
|
-
# @deprecated
|
|
183
|
+
# @deprecated
|
|
105
184
|
def with_tracked_errors(*tracked_errors)
|
|
106
|
-
|
|
185
|
+
deprecate(<<~MSG)
|
|
186
|
+
Light#with_tracked_errors is deprecated and will be removed in v6.0.0.
|
|
187
|
+
|
|
188
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
189
|
+
modifications.
|
|
190
|
+
|
|
191
|
+
Instead of:
|
|
192
|
+
light = Stoplight('api-call')
|
|
193
|
+
modified = light.with_tracked_errors(TimeoutError, NetworkError)
|
|
194
|
+
|
|
195
|
+
Configure correctly from the start:
|
|
196
|
+
Stoplight('api-call', tracked_errors: [TimeoutError, NetworkError])
|
|
197
|
+
MSG
|
|
198
|
+
with_without_warning(tracked_errors:)
|
|
107
199
|
end
|
|
108
200
|
|
|
109
201
|
# Configures a custom list of skipped errors that do not count toward the threshold.
|
|
@@ -120,11 +212,25 @@ module Stoplight
|
|
|
120
212
|
#
|
|
121
213
|
# @param skipped_errors [Array<Exception>]
|
|
122
214
|
# @return [Stoplight::Light]
|
|
123
|
-
# @deprecated
|
|
215
|
+
# @deprecated
|
|
124
216
|
def with_skipped_errors(*skipped_errors)
|
|
125
|
-
|
|
217
|
+
deprecate(<<~MSG)
|
|
218
|
+
Light#with_skipped_errors is deprecated and will be removed in v6.0.0.
|
|
219
|
+
|
|
220
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
221
|
+
modifications.
|
|
222
|
+
|
|
223
|
+
Instead of:
|
|
224
|
+
light = Stoplight('api-call')
|
|
225
|
+
modified = light.with_skipped_errors(ActiveRecord::RecordNotFound)
|
|
226
|
+
|
|
227
|
+
Configure correctly from the start:
|
|
228
|
+
Stoplight('api-call', skipped_errors: [ActiveRecord::RecordNotFound])
|
|
229
|
+
MSG
|
|
230
|
+
with_without_warning(skipped_errors:)
|
|
126
231
|
end
|
|
127
232
|
end
|
|
233
|
+
# steep:ignore:end
|
|
128
234
|
end
|
|
129
235
|
end
|
|
130
236
|
end
|
|
@@ -1,53 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "forwardable"
|
|
4
|
-
|
|
5
3
|
module Stoplight
|
|
6
4
|
module Domain
|
|
7
5
|
#
|
|
8
6
|
# @api private use +Stoplight()+ method instead
|
|
9
7
|
class Light
|
|
10
|
-
|
|
11
|
-
include ConfigurationBuilderInterface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
attr_reader :
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
8
|
+
include Common::Deprecations
|
|
9
|
+
include ConfigurationBuilderInterface # steep:ignore
|
|
10
|
+
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
attr_reader :green_run_strategy
|
|
14
|
+
attr_reader :yellow_run_strategy
|
|
15
|
+
attr_reader :red_run_strategy
|
|
16
|
+
attr_reader :factory
|
|
17
|
+
attr_reader :state_store
|
|
18
|
+
|
|
19
|
+
def initialize(name, green_run_strategy:, yellow_run_strategy:, red_run_strategy:, factory:, state_store:)
|
|
20
|
+
@name = name
|
|
47
21
|
@green_run_strategy = green_run_strategy
|
|
48
22
|
@yellow_run_strategy = yellow_run_strategy
|
|
49
23
|
@red_run_strategy = red_run_strategy
|
|
50
24
|
@factory = factory
|
|
25
|
+
@state_store = state_store
|
|
51
26
|
end
|
|
52
27
|
|
|
53
28
|
# Returns the current state of the light:
|
|
@@ -55,10 +30,7 @@ module Stoplight
|
|
|
55
30
|
# * +Stoplight::State::LOCKED_RED+ -- light is locked red and blocks all traffic
|
|
56
31
|
# * +Stoplight::State::UNLOCKED+ -- light is not locked and follow the configured rules
|
|
57
32
|
#
|
|
58
|
-
|
|
59
|
-
def state
|
|
60
|
-
state_snapshot.locked_state
|
|
61
|
-
end
|
|
33
|
+
def state = state_snapshot.locked_state
|
|
62
34
|
|
|
63
35
|
# Returns current color:
|
|
64
36
|
# * +Stoplight::Color::GREEN+ -- circuit breaker is closed
|
|
@@ -69,10 +41,7 @@ module Stoplight
|
|
|
69
41
|
# light = Stoplight('example')
|
|
70
42
|
# light.color #=> Color::GREEN
|
|
71
43
|
#
|
|
72
|
-
|
|
73
|
-
def color
|
|
74
|
-
state_snapshot.color
|
|
75
|
-
end
|
|
44
|
+
def color = state_snapshot.color
|
|
76
45
|
|
|
77
46
|
# Runs the given block of code with this circuit breaker
|
|
78
47
|
#
|
|
@@ -84,9 +53,7 @@ module Stoplight
|
|
|
84
53
|
# light = Stoplight('example')
|
|
85
54
|
# light.run(->(error) { 0 }) { 1 / 0 } #=> 0
|
|
86
55
|
#
|
|
87
|
-
# @param fallback
|
|
88
|
-
# @raise [Stoplight::Error::RedLight]
|
|
89
|
-
# @return [any]
|
|
56
|
+
# @param fallback fallback code to run if the circuit breaker is open
|
|
90
57
|
# @raise [Stoplight::Error::RedLight]
|
|
91
58
|
def run(fallback = nil, &code)
|
|
92
59
|
raise ArgumentError, "nothing to run. Please, pass a block into `Light#run`" unless block_given?
|
|
@@ -103,8 +70,8 @@ module Stoplight
|
|
|
103
70
|
# light = Stoplight('example-locked')
|
|
104
71
|
# light.lock(Stoplight::Color::RED)
|
|
105
72
|
#
|
|
106
|
-
# @param color
|
|
107
|
-
# @return
|
|
73
|
+
# @param color should be either +Color::RED+ or +Color::GREEN+
|
|
74
|
+
# @return locked light
|
|
108
75
|
def lock(color)
|
|
109
76
|
state = case color
|
|
110
77
|
when Color::RED then State::LOCKED_RED
|
|
@@ -112,7 +79,7 @@ module Stoplight
|
|
|
112
79
|
else raise Error::IncorrectColor
|
|
113
80
|
end
|
|
114
81
|
|
|
115
|
-
|
|
82
|
+
state_store.set_state(state)
|
|
116
83
|
|
|
117
84
|
self
|
|
118
85
|
end
|
|
@@ -124,21 +91,16 @@ module Stoplight
|
|
|
124
91
|
# light.lock(Stoplight::Color::RED)
|
|
125
92
|
# light.unlock
|
|
126
93
|
#
|
|
127
|
-
# @return
|
|
94
|
+
# @return returns unlocked light (circuit breaker)
|
|
128
95
|
def unlock
|
|
129
|
-
|
|
96
|
+
state_store.set_state(State::UNLOCKED)
|
|
130
97
|
|
|
131
98
|
self
|
|
132
99
|
end
|
|
133
100
|
|
|
134
101
|
# Two lights considered equal if they have the same configuration.
|
|
135
|
-
#
|
|
136
|
-
# @param other [any]
|
|
137
|
-
# @return [Boolean]
|
|
138
102
|
def ==(other)
|
|
139
|
-
other.is_a?(self.class) &&
|
|
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
|
|
103
|
+
other.is_a?(self.class) && factory == other.factory
|
|
142
104
|
end
|
|
143
105
|
|
|
144
106
|
# Reconfigures the light with updated settings and returns a new instance.
|
|
@@ -171,10 +133,30 @@ module Stoplight
|
|
|
171
133
|
# # Run the lights with their respective configurations
|
|
172
134
|
# invoices_light.run(->(error) { [] }) { call_invoices_api }
|
|
173
135
|
# payment_light.run(->(error) { nil }) { call_payment_api }
|
|
136
|
+
# @deprecated
|
|
174
137
|
# @see +Stoplight()+
|
|
138
|
+
# steep:ignore:start
|
|
175
139
|
def with(**settings)
|
|
140
|
+
deprecate(<<~MSG)
|
|
141
|
+
Light#with is deprecated and will be removed in v6.0.0.
|
|
142
|
+
|
|
143
|
+
Circuit breakers should be configured once at creation, not cloned with
|
|
144
|
+
modifications.
|
|
145
|
+
|
|
146
|
+
Instead of:
|
|
147
|
+
light = Stoplight('api-call', threshold: 5)
|
|
148
|
+
modified = light.with(threshold: 10)
|
|
149
|
+
|
|
150
|
+
Configure correctly from the start:
|
|
151
|
+
Stoplight('api-call', threshold: 10)
|
|
152
|
+
MSG
|
|
153
|
+
with_without_warning(**settings)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private def with_without_warning(**settings)
|
|
176
157
|
factory.build_with(**settings)
|
|
177
158
|
end
|
|
159
|
+
# steep:ignore:end
|
|
178
160
|
|
|
179
161
|
private
|
|
180
162
|
|
|
@@ -189,9 +171,7 @@ module Stoplight
|
|
|
189
171
|
end
|
|
190
172
|
end
|
|
191
173
|
|
|
192
|
-
def state_snapshot
|
|
193
|
-
data_store.get_state_snapshot(config)
|
|
194
|
-
end
|
|
174
|
+
def state_snapshot = state_store.state_snapshot
|
|
195
175
|
end
|
|
196
176
|
end
|
|
197
177
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stoplight
|
|
4
|
+
module Domain
|
|
5
|
+
# Request metrics over a given window.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
MetricsSnapshot = Data.define(
|
|
9
|
+
:successes,
|
|
10
|
+
:errors,
|
|
11
|
+
:consecutive_errors,
|
|
12
|
+
:consecutive_successes,
|
|
13
|
+
:last_error,
|
|
14
|
+
:last_success_at
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class MetricsSnapshot
|
|
18
|
+
# @!attribute successes
|
|
19
|
+
# A number of successes withing requested window. Zero for non-windowed metrics
|
|
20
|
+
#
|
|
21
|
+
# @!attribute errors
|
|
22
|
+
# A number of errors withing requested window. Zero for non-windowed metrics
|
|
23
|
+
|
|
24
|
+
# Calculates the error rate based on the number of successes and errors.
|
|
25
|
+
#
|
|
26
|
+
# @return [Float]
|
|
27
|
+
def error_rate
|
|
28
|
+
return unless requests # we effectively check if this is windowed metrics
|
|
29
|
+
|
|
30
|
+
if (successes! + errors!).zero?
|
|
31
|
+
0.0
|
|
32
|
+
else
|
|
33
|
+
errors!.fdiv(successes! + errors!)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
def requests
|
|
39
|
+
if successes && errors # we effectively check if this is windowed metrics
|
|
40
|
+
successes! + errors!
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Time, nil]
|
|
45
|
+
def last_error_at
|
|
46
|
+
last_error&.time
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def successes!
|
|
50
|
+
successes or raise TypeError, "success must not be nil"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def errors!
|
|
54
|
+
errors or raise TypeError, "errors must not be nil"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|