cmdx 2.0.0 → 2.1.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/CHANGELOG.md +62 -8
- data/lib/cmdx/callbacks.rb +31 -11
- data/lib/cmdx/chain.rb +29 -10
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +3 -9
- data/lib/cmdx/coercions/coerce.rb +4 -1
- data/lib/cmdx/coercions/date_time.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +11 -2
- data/lib/cmdx/coercions/symbol.rb +23 -4
- data/lib/cmdx/coercions.rb +25 -10
- data/lib/cmdx/configuration.rb +31 -16
- data/lib/cmdx/context.rb +40 -56
- data/lib/cmdx/deprecation.rb +4 -7
- data/lib/cmdx/deprecators/error.rb +4 -1
- data/lib/cmdx/deprecators.rb +17 -8
- data/lib/cmdx/errors.rb +15 -11
- data/lib/cmdx/executors/fiber.rb +16 -4
- data/lib/cmdx/executors/thread.rb +18 -4
- data/lib/cmdx/executors.rb +22 -7
- data/lib/cmdx/fault.rb +15 -3
- data/lib/cmdx/i18n_proxy.rb +10 -6
- data/lib/cmdx/input.rb +23 -21
- data/lib/cmdx/inputs.rb +14 -26
- data/lib/cmdx/log_formatters/json.rb +8 -1
- data/lib/cmdx/log_formatters/logstash.rb +7 -1
- data/lib/cmdx/mergers.rb +22 -7
- data/lib/cmdx/middlewares.rb +40 -24
- data/lib/cmdx/output.rb +5 -2
- data/lib/cmdx/pipeline.rb +28 -11
- data/lib/cmdx/railtie.rb +1 -0
- data/lib/cmdx/result.rb +22 -6
- data/lib/cmdx/retriers/decorrelated_jitter.rb +10 -5
- data/lib/cmdx/retriers/exponential.rb +10 -2
- data/lib/cmdx/retriers/fibonacci.rb +29 -12
- data/lib/cmdx/retriers.rb +17 -8
- data/lib/cmdx/retry.rb +20 -13
- data/lib/cmdx/runtime.rb +22 -40
- data/lib/cmdx/settings.rb +9 -9
- data/lib/cmdx/signal.rb +1 -1
- data/lib/cmdx/task.rb +90 -45
- data/lib/cmdx/telemetry.rb +52 -11
- data/lib/cmdx/util.rb +50 -4
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +15 -15
- data/lib/cmdx/validators/format.rb +12 -4
- data/lib/cmdx/validators/inclusion.rb +15 -15
- data/lib/cmdx/validators/length.rb +5 -49
- data/lib/cmdx/validators/numeric.rb +5 -49
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/validators/validate.rb +7 -1
- data/lib/cmdx/validators.rb +21 -9
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +28 -14
- data/lib/cmdx.rb +24 -0
- data/lib/generators/cmdx/templates/install.rb +96 -33
- data/mkdocs.yml +2 -0
- metadata +1 -1
|
@@ -11,27 +11,44 @@ module CMDx
|
|
|
11
11
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
|
+
# Hard cap on the index to keep multipliers/integer allocations bounded.
|
|
15
|
+
# `fib(78) < 2**63`, well past any realistic retry attempt. Pair with
|
|
16
|
+
# `:max_delay` for the actual sleep ceiling.
|
|
17
|
+
MAX_INDEX = 78
|
|
18
|
+
|
|
19
|
+
# Cache of computed Fibonacci numbers. Shared across calls so consecutive
|
|
20
|
+
# retries reuse prior work instead of recomputing from zero. Reads are
|
|
21
|
+
# lock-free; growth is performed on a local dup and swapped atomically
|
|
22
|
+
# under the mutex.
|
|
23
|
+
@cache = [0, 1].freeze
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
|
|
14
26
|
# @param attempt [Integer] zero-based retry attempt
|
|
15
27
|
# @param delay [Float] base delay in seconds
|
|
16
28
|
# @param _prev_delay [Float, nil] ignored
|
|
17
29
|
# @return [Float] computed delay
|
|
18
30
|
def call(attempt, delay, _prev_delay = nil)
|
|
19
|
-
|
|
31
|
+
index = attempt + 1
|
|
32
|
+
index = MAX_INDEX if index > MAX_INDEX
|
|
33
|
+
delay * fib(index)
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
private
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
def fib(n)
|
|
39
|
+
cache = @cache
|
|
40
|
+
return cache[n] if n < cache.size
|
|
41
|
+
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
cache = @cache
|
|
44
|
+
if cache.size <= n
|
|
45
|
+
grown = cache.dup
|
|
46
|
+
grown << (grown[-1] + grown[-2]) while grown.size <= n
|
|
47
|
+
@cache = grown.freeze
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@cache[n]
|
|
35
52
|
end
|
|
36
53
|
|
|
37
54
|
end
|
data/lib/cmdx/retriers.rb
CHANGED
|
@@ -41,9 +41,12 @@ module CMDx
|
|
|
41
41
|
retrier = callable || block
|
|
42
42
|
|
|
43
43
|
if callable && block
|
|
44
|
-
raise ArgumentError, "provide either a callable or a block, not both"
|
|
44
|
+
raise ArgumentError, "retrier: provide either a callable or a block, not both"
|
|
45
45
|
elsif !retrier.respond_to?(:call)
|
|
46
|
-
raise ArgumentError,
|
|
46
|
+
raise ArgumentError, <<~MSG.chomp
|
|
47
|
+
retrier must respond to #call (got #{retrier.class}).
|
|
48
|
+
See https://drexed.github.io/cmdx/retries/#custom-strategies-via-the-retriers-registry
|
|
49
|
+
MSG
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
registry[name.to_sym] = retrier
|
|
@@ -53,22 +56,25 @@ module CMDx
|
|
|
53
56
|
# @param name [Symbol]
|
|
54
57
|
# @return [Retriers] self for chaining
|
|
55
58
|
def deregister(name)
|
|
56
|
-
registry.delete(name
|
|
59
|
+
registry.delete(name)
|
|
57
60
|
self
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
# @param name [Symbol]
|
|
61
64
|
# @return [Boolean] whether a retrier is registered under `name`
|
|
62
65
|
def key?(name)
|
|
63
|
-
registry.key?(name
|
|
66
|
+
registry.key?(name)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
# @param name [Symbol]
|
|
67
70
|
# @return [#call] the registered retrier
|
|
68
|
-
# @raise [
|
|
71
|
+
# @raise [UnknownEntryError] when `name` isn't registered
|
|
69
72
|
def lookup(name)
|
|
70
73
|
registry[name] || begin
|
|
71
|
-
raise
|
|
74
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
75
|
+
unknown retrier #{name.inspect}; registered: #{registry.keys.inspect}.
|
|
76
|
+
See https://drexed.github.io/cmdx/retries/#built-in-strategies
|
|
77
|
+
MSG
|
|
72
78
|
end
|
|
73
79
|
end
|
|
74
80
|
|
|
@@ -78,7 +84,7 @@ module CMDx
|
|
|
78
84
|
#
|
|
79
85
|
# @param spec [Symbol, #call, nil]
|
|
80
86
|
# @return [#call, nil]
|
|
81
|
-
# @raise [
|
|
87
|
+
# @raise [UnknownEntryError] when `spec` is an unknown symbol or not callable
|
|
82
88
|
def resolve(spec)
|
|
83
89
|
case spec
|
|
84
90
|
when NilClass
|
|
@@ -88,7 +94,10 @@ module CMDx
|
|
|
88
94
|
else
|
|
89
95
|
return spec if spec.respond_to?(:call)
|
|
90
96
|
|
|
91
|
-
raise
|
|
97
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
98
|
+
unknown retrier #{spec.inspect}; expected a Symbol from #{registry.keys.inspect} or a callable.
|
|
99
|
+
See https://drexed.github.io/cmdx/retries/#built-in-strategies
|
|
100
|
+
MSG
|
|
92
101
|
end
|
|
93
102
|
end
|
|
94
103
|
|
data/lib/cmdx/retry.rb
CHANGED
|
@@ -20,6 +20,7 @@ module CMDx
|
|
|
20
20
|
# `:decorrelated_jitter`) or custom
|
|
21
21
|
# @yieldparam attempt [Integer]
|
|
22
22
|
# @yieldparam delay [Float]
|
|
23
|
+
# @yieldparam prev_delay [Float, nil]
|
|
23
24
|
def initialize(exceptions, options = EMPTY_HASH, &block)
|
|
24
25
|
@exceptions = exceptions.flatten
|
|
25
26
|
@options = options.freeze
|
|
@@ -28,15 +29,17 @@ module CMDx
|
|
|
28
29
|
|
|
29
30
|
# Returns a new Retry layering `new_exceptions` and `new_options` onto the
|
|
30
31
|
# current one. Used for inheritance so subclasses extend rather than
|
|
31
|
-
# replace.
|
|
32
|
+
# replace. Returns `self` only when *every* override (exceptions, options,
|
|
33
|
+
# and block) is empty so option-only updates such as `retry_on(limit: 5)`
|
|
34
|
+
# still take effect.
|
|
32
35
|
#
|
|
33
36
|
# @param new_exceptions [Array<Class>]
|
|
34
37
|
# @param new_options [Hash{Symbol => Object}]
|
|
35
38
|
# @param block [#call, nil] replacement jitter callable (falls back to the prior block)
|
|
36
|
-
# @yield [attempt, delay] optional replacement jitter block
|
|
39
|
+
# @yield [attempt, delay, prev_delay] optional replacement jitter block
|
|
37
40
|
# @return [Retry]
|
|
38
41
|
def build(new_exceptions, new_options, &block)
|
|
39
|
-
return self if new_exceptions.empty?
|
|
42
|
+
return self if new_exceptions.empty? && new_options.empty? && block.nil?
|
|
40
43
|
|
|
41
44
|
merged_exceptions = exceptions | new_exceptions.flatten
|
|
42
45
|
merged_options = @options.merge(new_options)
|
|
@@ -66,6 +69,16 @@ module CMDx
|
|
|
66
69
|
|
|
67
70
|
# Sleeps `attempt`'s jittered/bounded delay. No-op when the base delay is zero.
|
|
68
71
|
#
|
|
72
|
+
# Custom jitter callables (registry, task Symbol method, `Proc` / block via
|
|
73
|
+
# `instance_exec` on the task, and other `#call`-ables) always receive
|
|
74
|
+
# `(attempt, delay, prev_delay)` so strategies share one shape; ignore
|
|
75
|
+
# `prev_delay` when you do not need decorrelated threading.
|
|
76
|
+
#
|
|
77
|
+
# Non-numeric or non-finite jitter results are sanitized to the base `delay`
|
|
78
|
+
# and the final sleep is always clamped to `[0, max_delay]` when `max_delay`
|
|
79
|
+
# is set, preventing self-DoS from a buggy jitter returning `Float::INFINITY`
|
|
80
|
+
# or a non-Numeric value.
|
|
81
|
+
#
|
|
69
82
|
# @param attempt [Integer] zero-based retry attempt number
|
|
70
83
|
# @param task [Task, nil] used as receiver for Symbol/Proc jitter strategies
|
|
71
84
|
# @param prev_delay [Float, nil] previous computed delay; only consumed by
|
|
@@ -85,18 +98,19 @@ module CMDx
|
|
|
85
98
|
if registry.key?(jitter)
|
|
86
99
|
registry.lookup(jitter).call(attempt, delay, prev_delay)
|
|
87
100
|
else
|
|
88
|
-
task.send(jitter, attempt, delay)
|
|
101
|
+
task.send(jitter, attempt, delay, prev_delay)
|
|
89
102
|
end
|
|
90
103
|
when Proc
|
|
91
|
-
task.instance_exec(attempt, delay, &jitter)
|
|
104
|
+
task.instance_exec(attempt, delay, prev_delay, &jitter)
|
|
92
105
|
else
|
|
93
106
|
if jitter.respond_to?(:call)
|
|
94
|
-
jitter.call(attempt, delay)
|
|
107
|
+
jitter.call(attempt, delay, prev_delay)
|
|
95
108
|
else
|
|
96
109
|
delay
|
|
97
110
|
end
|
|
98
111
|
end
|
|
99
112
|
|
|
113
|
+
d = delay unless d.is_a?(Numeric) && d.finite?
|
|
100
114
|
d = d.clamp(0, max_delay) if max_delay
|
|
101
115
|
Kernel.sleep(d) if d.positive?
|
|
102
116
|
d
|
|
@@ -126,13 +140,6 @@ module CMDx
|
|
|
126
140
|
|
|
127
141
|
private
|
|
128
142
|
|
|
129
|
-
# Resolves the retriers registry to consult for built-in jitter strategies.
|
|
130
|
-
# Prefers the task class's registry (so per-task `register(:retrier, ...)`
|
|
131
|
-
# overrides take effect) and falls back to the global configuration when
|
|
132
|
-
# no task is supplied.
|
|
133
|
-
#
|
|
134
|
-
# @param task [Task, nil]
|
|
135
|
-
# @return [Retriers]
|
|
136
143
|
def retriers_registry(task)
|
|
137
144
|
if task && task.class.respond_to?(:retriers)
|
|
138
145
|
task.class.retriers
|
data/lib/cmdx/runtime.rb
CHANGED
|
@@ -6,13 +6,14 @@ module CMDx
|
|
|
6
6
|
# retry), output verification, rollback on failure, result finalization,
|
|
7
7
|
# and teardown (freeze + chain clear).
|
|
8
8
|
#
|
|
9
|
-
# Signal propagation:
|
|
10
|
-
# `success!` / `skip!` / `fail!` /
|
|
11
|
-
#
|
|
12
|
-
# result as `:origin`); other `StandardError`s become
|
|
13
|
-
# the exception as `:cause
|
|
14
|
-
#
|
|
15
|
-
# originating result so `fault.task` points at the leaf that
|
|
9
|
+
# Signal propagation: `work` and the surrounding middleware/callback chain
|
|
10
|
+
# both run inside `catch(Signal::TAG)`, so `success!` / `skip!` / `fail!` /
|
|
11
|
+
# `throw!` halt cleanly from anywhere. Raised Faults become echoed signals
|
|
12
|
+
# (with the upstream result as `:origin`); other `StandardError`s become
|
|
13
|
+
# failed signals (with the exception as `:cause`). In strict mode,
|
|
14
|
+
# `execute!` re-raises from `ensure` using a {Fault} built from the
|
|
15
|
+
# deepest originating result, so `fault.task` points at the leaf that
|
|
16
|
+
# failed.
|
|
16
17
|
#
|
|
17
18
|
# @note Always used via the class method; never new Runtime manually.
|
|
18
19
|
# @see Task.execute
|
|
@@ -51,13 +52,12 @@ module CMDx
|
|
|
51
52
|
emit_telemetry(:task_started)
|
|
52
53
|
run_deprecation
|
|
53
54
|
run_lifecycle
|
|
54
|
-
finalize_result
|
|
55
|
-
raise_signal! if @strict
|
|
56
55
|
end
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
finalize_result
|
|
59
58
|
ensure
|
|
60
59
|
run_teardown
|
|
60
|
+
raise_signal!
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
private
|
|
@@ -71,10 +71,11 @@ module CMDx
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def run_middlewares(&)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
@signal = catch(Signal::TAG) do
|
|
75
|
+
middlewares = @task.class.middlewares
|
|
76
|
+
middlewares.empty? ? yield : middlewares.process(@task, &)
|
|
77
|
+
@signal # Return non-middleware signal
|
|
78
|
+
end
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def run_deprecation
|
|
@@ -90,12 +91,12 @@ module CMDx
|
|
|
90
91
|
def run_lifecycle
|
|
91
92
|
measure_duration do
|
|
92
93
|
run_callbacks(:before_execution)
|
|
94
|
+
run_callbacks(:before_validation)
|
|
93
95
|
run_around(:around_execution) do
|
|
94
|
-
run_callbacks(:before_validation)
|
|
95
96
|
perform_work
|
|
96
97
|
perform_rollback if @signal.failed?
|
|
97
|
-
run_callbacks(:after_execution)
|
|
98
98
|
end
|
|
99
|
+
run_callbacks(:after_execution)
|
|
99
100
|
run_callbacks(:"on_#{@signal.state}")
|
|
100
101
|
run_callbacks(:"on_#{@signal.status}")
|
|
101
102
|
run_callbacks(:on_ok) if @signal.ok?
|
|
@@ -104,12 +105,12 @@ module CMDx
|
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
def raise_signal!
|
|
107
|
-
return unless @result
|
|
108
|
+
return unless @strict && @result&.failed?
|
|
108
109
|
|
|
109
|
-
cause = @
|
|
110
|
+
cause = @result.cause
|
|
110
111
|
raise cause if cause && !cause.is_a?(Fault)
|
|
111
112
|
|
|
112
|
-
raise Fault, @result.caused_failure
|
|
113
|
+
raise Fault, @result.caused_failure, cause:
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
def finalize_result
|
|
@@ -151,8 +152,6 @@ module CMDx
|
|
|
151
152
|
@duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
|
|
152
153
|
end
|
|
153
154
|
|
|
154
|
-
# @param event [Symbol] callback event from {Callbacks::EVENTS}
|
|
155
|
-
# @return [void]
|
|
156
155
|
def run_callbacks(event)
|
|
157
156
|
callbacks = @task.class.callbacks
|
|
158
157
|
return if callbacks.empty?
|
|
@@ -160,9 +159,6 @@ module CMDx
|
|
|
160
159
|
callbacks.process(event, @task)
|
|
161
160
|
end
|
|
162
161
|
|
|
163
|
-
# @param event [Symbol] callback event from {Callbacks::EVENTS}
|
|
164
|
-
# @yield nested lifecycle segment wrapped by `around_execution` callbacks
|
|
165
|
-
# @return [Object] the wrapped block's return value
|
|
166
162
|
def run_around(event, &)
|
|
167
163
|
callbacks = @task.class.callbacks
|
|
168
164
|
return yield if callbacks.empty?
|
|
@@ -181,7 +177,7 @@ module CMDx
|
|
|
181
177
|
rescue Error => e
|
|
182
178
|
raise(e)
|
|
183
179
|
rescue StandardError => e
|
|
184
|
-
Signal.failed(
|
|
180
|
+
Signal.failed(Util.to_error_s(e), cause: e, metadata: @task.metadata)
|
|
185
181
|
end
|
|
186
182
|
end
|
|
187
183
|
|
|
@@ -225,25 +221,11 @@ module CMDx
|
|
|
225
221
|
throw(Signal::TAG, Signal.failed(@task.errors.to_s, metadata: @task.metadata))
|
|
226
222
|
end
|
|
227
223
|
|
|
228
|
-
# @param name [Symbol] telemetry channel from {Telemetry::EVENTS}
|
|
229
|
-
# @param payload [Hash{Symbol => Object}] forwarded onto {Telemetry::Event#payload}
|
|
230
|
-
# @return [void]
|
|
231
224
|
def emit_telemetry(name, payload = EMPTY_HASH)
|
|
232
225
|
telemetry = @task.class.telemetry
|
|
233
226
|
return unless telemetry.subscribed?(name)
|
|
234
227
|
|
|
235
|
-
event = Telemetry::Event.
|
|
236
|
-
xid: Chain.current.xid,
|
|
237
|
-
cid: Chain.current.id,
|
|
238
|
-
root: @root,
|
|
239
|
-
type: @task.class.type,
|
|
240
|
-
task: @task.class,
|
|
241
|
-
tid: @task.tid,
|
|
242
|
-
name:,
|
|
243
|
-
payload:,
|
|
244
|
-
timestamp: Time.now.utc
|
|
245
|
-
)
|
|
246
|
-
|
|
228
|
+
event = Telemetry::Event.build(@task, name, root: @root, payload:)
|
|
247
229
|
telemetry.emit(name, event)
|
|
248
230
|
end
|
|
249
231
|
|
data/lib/cmdx/settings.rb
CHANGED
|
@@ -50,7 +50,10 @@ module CMDx
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
# @return [Array<Symbol>] keys to exclude from
|
|
53
|
+
# @return [Array<Symbol>] keys to exclude from `Runtime` log output.
|
|
54
|
+
# Matched against {Result#to_h} keys. Common values for redaction:
|
|
55
|
+
# `:context` (may contain secrets / PII), `:cause` (raw exception),
|
|
56
|
+
# `:reason` (may embed exception messages from unhandled errors).
|
|
54
57
|
def log_exclusions
|
|
55
58
|
@options.fetch(:log_exclusions) do
|
|
56
59
|
CMDx.configuration.log_exclusions
|
|
@@ -64,14 +67,9 @@ module CMDx
|
|
|
64
67
|
end
|
|
65
68
|
end
|
|
66
69
|
|
|
67
|
-
#
|
|
68
|
-
# without affecting other tasks (or hitting `FrozenError` on the
|
|
69
|
-
# shared sentinel).
|
|
70
|
-
#
|
|
71
|
-
# @return [Array<Symbol, String>] task tags, surfaced on result hashes
|
|
70
|
+
# @return [Array<Symbol, String>] task tags
|
|
72
71
|
def tags
|
|
73
|
-
|
|
74
|
-
tags ? tags.dup : []
|
|
72
|
+
@options[:tags] || EMPTY_ARRAY
|
|
75
73
|
end
|
|
76
74
|
|
|
77
75
|
# @return [Boolean] whether this task's {Context} should raise on
|
|
@@ -83,7 +81,9 @@ module CMDx
|
|
|
83
81
|
end
|
|
84
82
|
end
|
|
85
83
|
|
|
86
|
-
# @return [
|
|
84
|
+
# @return [#call, nil] callable that produces a correlation id when invoked
|
|
85
|
+
# by Runtime at root-chain construction. Resolution order:
|
|
86
|
+
# task-level setting → {Configuration#correlation_id} → nil.
|
|
87
87
|
def correlation_id
|
|
88
88
|
@options.fetch(:correlation_id) do
|
|
89
89
|
CMDx.configuration.correlation_id
|
data/lib/cmdx/signal.rb
CHANGED
|
@@ -76,7 +76,7 @@ module CMDx
|
|
|
76
76
|
# @return [Signal] new instance mirroring `other`
|
|
77
77
|
# @raise [ArgumentError] when `other` is neither a Signal nor a Result
|
|
78
78
|
def echoed(other, **options)
|
|
79
|
-
raise ArgumentError, "
|
|
79
|
+
raise ArgumentError, "Signal.echoed expected a Result or Signal (got #{other.class})" unless other.is_a?(Result) || other.is_a?(Signal)
|
|
80
80
|
|
|
81
81
|
options[:origin] = other if other.is_a?(Result) && !options.key?(:origin)
|
|
82
82
|
new(other.state, other.status, **options, reason: other.reason)
|
data/lib/cmdx/task.rb
CHANGED
|
@@ -53,6 +53,9 @@ module CMDx
|
|
|
53
53
|
# @option options [Array<Symbol>] :log_exclusions (see {Settings#initialize})
|
|
54
54
|
# @option options [Array<Symbol, String>] :tags (see {Settings#initialize})
|
|
55
55
|
# @option options [Boolean] :strict_context (see {Settings#initialize})
|
|
56
|
+
# @option options [#call] :correlation_id callable returning a String id
|
|
57
|
+
# resolved once by {Runtime} when the root chain is acquired; surfaces
|
|
58
|
+
# as `result.xid` (see {Settings#correlation_id})
|
|
56
59
|
# @return [Settings]
|
|
57
60
|
def settings(options = EMPTY_HASH)
|
|
58
61
|
@settings ||=
|
|
@@ -190,7 +193,12 @@ module CMDx
|
|
|
190
193
|
inputs.register(self, ...)
|
|
191
194
|
when :output
|
|
192
195
|
outputs.register(...)
|
|
193
|
-
else
|
|
196
|
+
else
|
|
197
|
+
raise ArgumentError, <<~MSG.chomp
|
|
198
|
+
unknown registry type #{type.inspect};
|
|
199
|
+
expected one of [:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output].
|
|
200
|
+
See https://drexed.github.io/cmdx/configuration/#registrations-register-deregister
|
|
201
|
+
MSG
|
|
194
202
|
end
|
|
195
203
|
end
|
|
196
204
|
|
|
@@ -221,7 +229,12 @@ module CMDx
|
|
|
221
229
|
inputs.deregister(self, ...)
|
|
222
230
|
when :output
|
|
223
231
|
outputs.deregister(...)
|
|
224
|
-
else
|
|
232
|
+
else
|
|
233
|
+
raise ArgumentError, <<~MSG.chomp
|
|
234
|
+
unknown registry type #{type.inspect};
|
|
235
|
+
expected one of [:middleware, :callback, :coercion, :validator, :executor, :merger, :retrier, :deprecator, :input, :output].
|
|
236
|
+
See https://drexed.github.io/cmdx/configuration/#registrations-register-deregister
|
|
237
|
+
MSG
|
|
225
238
|
end
|
|
226
239
|
end
|
|
227
240
|
|
|
@@ -280,6 +293,8 @@ module CMDx
|
|
|
280
293
|
alias input inputs
|
|
281
294
|
|
|
282
295
|
# Declares optional inputs (shorthand for `inputs ..., required: false`).
|
|
296
|
+
# An explicit `required:` in `options` is ignored — use {.inputs} when
|
|
297
|
+
# you need to set the flag dynamically.
|
|
283
298
|
#
|
|
284
299
|
# @param names [Array<Symbol>]
|
|
285
300
|
# @param options [Hash{Symbol => Object}] see {Input#initialize}
|
|
@@ -296,10 +311,12 @@ module CMDx
|
|
|
296
311
|
# @option options [Object] :validate (see {Validators#extract})
|
|
297
312
|
# @yield nested-input DSL block (see {Inputs::ChildBuilder})
|
|
298
313
|
def optional(*names, **options, &)
|
|
299
|
-
register(:input, *names, required: false,
|
|
314
|
+
register(:input, *names, **options, required: false, &)
|
|
300
315
|
end
|
|
301
316
|
|
|
302
317
|
# Declares required inputs (shorthand for `inputs ..., required: true`).
|
|
318
|
+
# An explicit `required:` in `options` is ignored — use {.inputs} when
|
|
319
|
+
# you need to set the flag dynamically.
|
|
303
320
|
#
|
|
304
321
|
# @param names [Array<Symbol>]
|
|
305
322
|
# @param options [Hash{Symbol => Object}] see {Input#initialize}
|
|
@@ -316,7 +333,7 @@ module CMDx
|
|
|
316
333
|
# @option options [Object] :validate (see {Validators#extract})
|
|
317
334
|
# @yield nested-input DSL block (see {Inputs::ChildBuilder})
|
|
318
335
|
def required(*names, **options, &)
|
|
319
|
-
register(:input, *names, required: true,
|
|
336
|
+
register(:input, *names, **options, required: true, &)
|
|
320
337
|
end
|
|
321
338
|
|
|
322
339
|
# @return [Hash{Symbol => Hash}] serialized input definitions
|
|
@@ -382,23 +399,29 @@ module CMDx
|
|
|
382
399
|
|
|
383
400
|
private
|
|
384
401
|
|
|
385
|
-
# @param input [Input] defines `##{input.accessor_name}` when not already taken
|
|
386
|
-
# @return [void]
|
|
387
|
-
# @raise [DefinitionError] when the accessor name collides
|
|
388
402
|
def define_input_reader(input)
|
|
389
403
|
accessor = input.accessor_name
|
|
390
404
|
|
|
391
405
|
if method_defined?(accessor) || private_method_defined?(accessor)
|
|
392
|
-
raise DefinitionError,
|
|
393
|
-
|
|
406
|
+
raise DefinitionError, <<~MSG.chomp
|
|
407
|
+
Cannot define input #{accessor.inspect}: ##{accessor} is already defined on #{self}.
|
|
408
|
+
|
|
409
|
+
Typical cause #1: there's a method or private method with the same name as the input.
|
|
410
|
+
|
|
411
|
+
Typical cause #2: two sibling `inputs` groups declare the same nested name
|
|
412
|
+
(for example `inputs :a, :b do ... required :x ... end`, which defines `:x` twice).
|
|
413
|
+
|
|
414
|
+
Fix: rename with `:as`, `:prefix`, or `:suffix`, or give each parent its own
|
|
415
|
+
`inputs ... do ... end` block so nested accessors do not collide.
|
|
416
|
+
|
|
417
|
+
https://drexed.github.io/cmdx/inputs/definitions
|
|
418
|
+
MSG
|
|
394
419
|
end
|
|
395
420
|
|
|
396
421
|
define_method(accessor) { instance_variable_get(input.ivar_name) }
|
|
397
422
|
input.children.each { |child| define_input_reader(child) }
|
|
398
423
|
end
|
|
399
424
|
|
|
400
|
-
# @param input [Input] removes `##{input.accessor_name}` if defined on this class
|
|
401
|
-
# @return [void]
|
|
402
425
|
def undefine_input_reader(input)
|
|
403
426
|
accessor = input.accessor_name
|
|
404
427
|
undef_method(accessor) if method_defined?(accessor)
|
|
@@ -449,65 +472,87 @@ module CMDx
|
|
|
449
472
|
# @return [void]
|
|
450
473
|
# @raise [ImplementationError] when the subclass doesn't override
|
|
451
474
|
def work
|
|
452
|
-
raise ImplementationError,
|
|
475
|
+
raise ImplementationError, <<~MSG.chomp
|
|
476
|
+
#{self.class} must implement #work.
|
|
477
|
+
See https://drexed.github.io/cmdx/basics/setup/#structure
|
|
478
|
+
MSG
|
|
453
479
|
end
|
|
454
480
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
# Signals a successful halt.
|
|
481
|
+
# Halts `#work` early with a successful outcome. Any `sigdata` is merged
|
|
482
|
+
# onto {#metadata} before the signal is thrown.
|
|
458
483
|
#
|
|
459
|
-
# @param reason [String, nil]
|
|
460
|
-
# @param sigdata [Hash{Symbol => Object}]
|
|
461
|
-
# @
|
|
462
|
-
# @
|
|
463
|
-
# @raise [FrozenError] when the task has already been frozen (post-execution)
|
|
464
|
-
# @note Must be called from inside `work` (inside Runtime's `catch(:cmdx_signal)`).
|
|
484
|
+
# @param reason [String, nil] human-readable explanation surfaced on the {Result}
|
|
485
|
+
# @param sigdata [Hash{Symbol => Object}] extra metadata merged into {#metadata}
|
|
486
|
+
# @return [void] never returns; throws {Signal::TAG}
|
|
487
|
+
# @raise [FrozenTaskError] when the task has already been executed
|
|
465
488
|
def success!(reason = nil, **sigdata)
|
|
466
|
-
|
|
489
|
+
if frozen?
|
|
490
|
+
raise FrozenTaskError, <<~MSG.chomp
|
|
491
|
+
cannot call :success! on #{self.class}; the task has already been executed and frozen.
|
|
492
|
+
See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
|
|
493
|
+
MSG
|
|
494
|
+
end
|
|
467
495
|
|
|
468
496
|
metadata.merge!(sigdata) unless sigdata.empty?
|
|
469
497
|
throw(Signal::TAG, Signal.success(reason, metadata:))
|
|
470
498
|
end
|
|
471
499
|
|
|
472
|
-
#
|
|
500
|
+
# Halts `#work` and marks the {Result} as `skipped`. Any `sigdata` is merged
|
|
501
|
+
# onto {#metadata} before the signal is thrown.
|
|
473
502
|
#
|
|
474
|
-
# @param reason [String, nil]
|
|
475
|
-
# @param sigdata [Hash{Symbol => Object}]
|
|
476
|
-
# @
|
|
477
|
-
# @
|
|
478
|
-
# @raise [FrozenError]
|
|
503
|
+
# @param reason [String, nil] human-readable explanation surfaced on the {Result}
|
|
504
|
+
# @param sigdata [Hash{Symbol => Object}] extra metadata merged into {#metadata}
|
|
505
|
+
# @return [void] never returns; throws {Signal::TAG}
|
|
506
|
+
# @raise [FrozenTaskError] when the task has already been executed
|
|
479
507
|
def skip!(reason = nil, **sigdata)
|
|
480
|
-
|
|
508
|
+
if frozen?
|
|
509
|
+
raise FrozenTaskError, <<~MSG.chomp
|
|
510
|
+
cannot call :skip! on #{self.class}; the task has already been executed and frozen.
|
|
511
|
+
See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
|
|
512
|
+
MSG
|
|
513
|
+
end
|
|
481
514
|
|
|
482
515
|
metadata.merge!(sigdata) unless sigdata.empty?
|
|
483
516
|
throw(Signal::TAG, Signal.skipped(reason, metadata:))
|
|
484
517
|
end
|
|
485
518
|
|
|
486
|
-
#
|
|
487
|
-
#
|
|
519
|
+
# Halts `#work` and marks the {Result} as `failed`. Captures the current
|
|
520
|
+
# caller frames for the {Fault} backtrace and merges any `sigdata` onto
|
|
521
|
+
# {#metadata} before the signal is thrown.
|
|
488
522
|
#
|
|
489
|
-
# @param reason [String, nil]
|
|
490
|
-
# @param sigdata [Hash{Symbol => Object}]
|
|
491
|
-
# @
|
|
492
|
-
# @
|
|
493
|
-
# @raise [FrozenError]
|
|
523
|
+
# @param reason [String, nil] human-readable explanation surfaced on the {Result}/{Fault}
|
|
524
|
+
# @param sigdata [Hash{Symbol => Object}] extra metadata merged into {#metadata}
|
|
525
|
+
# @return [void] never returns; throws {Signal::TAG}
|
|
526
|
+
# @raise [FrozenTaskError] when the task has already been executed
|
|
494
527
|
def fail!(reason = nil, **sigdata)
|
|
495
|
-
|
|
528
|
+
if frozen?
|
|
529
|
+
raise FrozenTaskError, <<~MSG.chomp
|
|
530
|
+
cannot call :fail! on #{self.class}; the task has already been executed and frozen.
|
|
531
|
+
See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
|
|
532
|
+
MSG
|
|
533
|
+
end
|
|
496
534
|
|
|
497
535
|
metadata.merge!(sigdata) unless sigdata.empty?
|
|
498
536
|
throw(Signal::TAG, Signal.failed(reason, metadata:, backtrace: caller_locations(1)))
|
|
499
537
|
end
|
|
500
538
|
|
|
501
|
-
#
|
|
502
|
-
# `other`
|
|
539
|
+
# Echoes another {Result} or {Signal}'s failure outcome from this task. A
|
|
540
|
+
# no-op when `other` is not failed, letting callers conditionally bubble up
|
|
541
|
+
# nested failures without branching. Captures caller frames for the {Fault}
|
|
542
|
+
# backtrace and merges any `sigdata` onto {#metadata} before the signal is
|
|
543
|
+
# thrown.
|
|
503
544
|
#
|
|
504
|
-
# @param other [Result]
|
|
505
|
-
# @param sigdata [Hash{Symbol => Object}]
|
|
506
|
-
# @
|
|
507
|
-
# @
|
|
508
|
-
# @raise [FrozenError]
|
|
545
|
+
# @param other [Result, Signal] upstream outcome to mirror
|
|
546
|
+
# @param sigdata [Hash{Symbol => Object}] extra metadata merged into {#metadata}
|
|
547
|
+
# @return [void, nil] returns `nil` when `other` is not failed; otherwise throws {Signal::TAG}
|
|
548
|
+
# @raise [FrozenTaskError] when the task has already been executed
|
|
509
549
|
def throw!(other, **sigdata)
|
|
510
|
-
|
|
550
|
+
if frozen?
|
|
551
|
+
raise FrozenTaskError, <<~MSG.chomp
|
|
552
|
+
cannot call :throw! on #{self.class}; the task has already been executed and frozen.
|
|
553
|
+
See https://drexed.github.io/cmdx/outcomes/result/#lifecycle-predicates
|
|
554
|
+
MSG
|
|
555
|
+
end
|
|
511
556
|
|
|
512
557
|
return unless other.failed?
|
|
513
558
|
|