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
data/lib/cmdx/context.rb
CHANGED
|
@@ -47,10 +47,25 @@ module CMDx
|
|
|
47
47
|
elsif context.respond_to?(:to_h)
|
|
48
48
|
context.to_h
|
|
49
49
|
else
|
|
50
|
-
raise ArgumentError,
|
|
50
|
+
raise ArgumentError, <<~MSG.chomp
|
|
51
|
+
Context.build expected a Hash or an object responding to #to_h/#to_hash (got #{context.class}).
|
|
52
|
+
See https://drexed.github.io/cmdx/basics/context/#assigning-data
|
|
53
|
+
MSG
|
|
51
54
|
end.transform_keys(&:to_sym)
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
# Ensures `#dup` / `#clone` produce a context with an independent backing
|
|
58
|
+
# hash so mutations on the copy do not leak into the original. `@strict`
|
|
59
|
+
# is preserved on the copy.
|
|
60
|
+
#
|
|
61
|
+
# @param source [Context]
|
|
62
|
+
# @return [void]
|
|
63
|
+
def initialize_copy(source)
|
|
64
|
+
super
|
|
65
|
+
@table = source.instance_variable_get(:@table).dup
|
|
66
|
+
@strict = source.strict?
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
# @return [Boolean] whether dynamic reads for unknown keys raise instead
|
|
55
70
|
# of returning `nil`
|
|
56
71
|
def strict?
|
|
@@ -87,7 +102,7 @@ module CMDx
|
|
|
87
102
|
# @return [Context] self for chaining
|
|
88
103
|
def deep_merge(context = EMPTY_HASH)
|
|
89
104
|
other = self.class.build(context)
|
|
90
|
-
@table =
|
|
105
|
+
@table = Util.deep_merge(@table, other.to_h)
|
|
91
106
|
self
|
|
92
107
|
end
|
|
93
108
|
|
|
@@ -202,9 +217,11 @@ module CMDx
|
|
|
202
217
|
@table.hash
|
|
203
218
|
end
|
|
204
219
|
|
|
205
|
-
# @return [Hash{Symbol => Object}] the underlying table
|
|
220
|
+
# @return [Hash{Symbol => Object}] a shallow copy of the underlying table.
|
|
221
|
+
# Frozen contexts return the frozen table directly to preserve
|
|
222
|
+
# `Hash#frozen?` semantics for serialization callers.
|
|
206
223
|
def to_h
|
|
207
|
-
@table
|
|
224
|
+
@table.frozen? ? @table : @table.dup
|
|
208
225
|
end
|
|
209
226
|
|
|
210
227
|
# JSON-friendly hash view. Aliases {#to_h} for conventional `as_json`
|
|
@@ -227,7 +244,14 @@ module CMDx
|
|
|
227
244
|
|
|
228
245
|
# @return [String] space-separated `key=value.inspect` pairs
|
|
229
246
|
def to_s
|
|
230
|
-
|
|
247
|
+
buf = String.new(capacity: 256)
|
|
248
|
+
|
|
249
|
+
@table.each do |k, v|
|
|
250
|
+
buf << " " unless buf.empty?
|
|
251
|
+
buf << k.to_s << "=" << v.inspect
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
buf
|
|
231
255
|
end
|
|
232
256
|
|
|
233
257
|
# Pattern-matching support for `case context in {...}`.
|
|
@@ -247,12 +271,13 @@ module CMDx
|
|
|
247
271
|
|
|
248
272
|
# Returns a deep copy. Non-mutable scalars are shared; Hashes/Arrays are
|
|
249
273
|
# recursively duplicated; other objects fall back to `#dup` (and then
|
|
250
|
-
# to the original on `StandardError`).
|
|
274
|
+
# to the original on `StandardError`). `@strict` is preserved on the copy.
|
|
251
275
|
#
|
|
252
276
|
# @return [Context]
|
|
253
277
|
def deep_dup
|
|
254
278
|
ctx = self.class.allocate
|
|
255
|
-
ctx.instance_variable_set(:@table,
|
|
279
|
+
ctx.instance_variable_set(:@table, Util.deep_dup(@table))
|
|
280
|
+
ctx.instance_variable_set(:@strict, @strict)
|
|
256
281
|
ctx
|
|
257
282
|
end
|
|
258
283
|
|
|
@@ -267,65 +292,24 @@ module CMDx
|
|
|
267
292
|
|
|
268
293
|
private
|
|
269
294
|
|
|
270
|
-
# Provides dynamic read/write/predicate access to context keys.
|
|
271
|
-
#
|
|
272
|
-
# - `ctx.name` — reads `@table[name]`, `nil` when absent (raises
|
|
273
|
-
# `NoMethodError` when {#strict?} is true and the key is absent).
|
|
274
|
-
# - `ctx.name = val` — stores `val` under `:name`.
|
|
275
|
-
# - `ctx.name?` — truthy check for `@table[:name]`.
|
|
276
|
-
#
|
|
277
|
-
# @param method_name [Symbol] dynamic reader/writer/predicate name
|
|
278
|
-
# @param args [Array<Object>] stores RHS for writers (`name=` → `[value]`)
|
|
279
|
-
# @param _kwargs [Hash{Symbol => Object}] ignored (accepted for Ruby keyword forwarding)
|
|
280
|
-
# @option _kwargs [Object] ignored
|
|
281
|
-
# @raise [NoMethodError] when {#strict?} is true and the key is missing
|
|
282
|
-
# @api private
|
|
283
295
|
def method_missing(method_name, *args, **_kwargs, &)
|
|
284
|
-
if
|
|
296
|
+
if @table.key?(method_name)
|
|
297
|
+
@table[method_name]
|
|
298
|
+
elsif method_name.end_with?("=")
|
|
285
299
|
@table[method_name[..-2].to_sym] = args.first
|
|
286
300
|
elsif method_name.end_with?("?")
|
|
287
301
|
!!@table[method_name[..-2].to_sym]
|
|
288
|
-
elsif strict?
|
|
289
|
-
raise
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
elsif strict?
|
|
303
|
+
raise UnknownAccessorError, <<~MSG.chomp
|
|
304
|
+
unknown context key #{method_name.inspect} (strict mode); declared keys: #{@table.keys.inspect}.
|
|
305
|
+
See https://drexed.github.io/cmdx/basics/context/#strict-mode
|
|
306
|
+
MSG
|
|
292
307
|
end
|
|
293
308
|
end
|
|
294
309
|
|
|
295
|
-
# @param method_name [Symbol]
|
|
296
|
-
# @param include_private [Boolean] forwarded to Ruby's `respond_to?` lookup
|
|
297
|
-
# @return [Boolean]
|
|
298
310
|
def respond_to_missing?(method_name, include_private = false)
|
|
299
311
|
@table.key?(method_name) || method_name.end_with?("=", "?") || super
|
|
300
312
|
end
|
|
301
313
|
|
|
302
|
-
# @param value [Object] nested value from the context table
|
|
303
|
-
# @return [Object] recursively duplicated scalar/collection snapshot
|
|
304
|
-
def compute_deep_dup(value)
|
|
305
|
-
case value
|
|
306
|
-
when Numeric, Symbol, TrueClass, FalseClass, NilClass
|
|
307
|
-
value
|
|
308
|
-
when Hash
|
|
309
|
-
value.each_with_object({}) { |(k, v), acc| acc[k] = compute_deep_dup(v) }
|
|
310
|
-
when Array
|
|
311
|
-
value.map { |e| compute_deep_dup(e) }
|
|
312
|
-
else
|
|
313
|
-
begin
|
|
314
|
-
value.dup
|
|
315
|
-
rescue StandardError
|
|
316
|
-
value
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
# @param lhs [Hash]
|
|
322
|
-
# @param rhs [Hash]
|
|
323
|
-
# @return [Hash] merged hash (recursive for nested `{Hash => Hash}` pairs)
|
|
324
|
-
def compute_deep_merge(lhs, rhs)
|
|
325
|
-
lhs.merge(rhs) do |_key, l, r|
|
|
326
|
-
l.is_a?(Hash) && r.is_a?(Hash) ? compute_deep_merge(l, r) : r
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
314
|
end
|
|
331
315
|
end
|
data/lib/cmdx/deprecation.rb
CHANGED
|
@@ -43,18 +43,15 @@ module CMDx
|
|
|
43
43
|
else
|
|
44
44
|
return @value.call(task) if @value.respond_to?(:call)
|
|
45
45
|
|
|
46
|
-
raise ArgumentError,
|
|
46
|
+
raise ArgumentError, <<~MSG.chomp
|
|
47
|
+
deprecation must be a Symbol, Proc, or respond to #call (got #{@value.class}).
|
|
48
|
+
See https://drexed.github.io/cmdx/deprecation/#declarations
|
|
49
|
+
MSG
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
private
|
|
51
54
|
|
|
52
|
-
# Resolves the deprecators registry to consult for built-in actions.
|
|
53
|
-
# Prefers the task class's registry (so per-task `register(:deprecator, ...)`
|
|
54
|
-
# overrides take effect) and falls back to the global configuration.
|
|
55
|
-
#
|
|
56
|
-
# @param task [Task]
|
|
57
|
-
# @return [Deprecators]
|
|
58
55
|
def deprecators_registry(task)
|
|
59
56
|
if task.class.respond_to?(:deprecators)
|
|
60
57
|
task.class.deprecators
|
|
@@ -14,7 +14,10 @@ module CMDx
|
|
|
14
14
|
# @return [void]
|
|
15
15
|
# @raise [DeprecationError]
|
|
16
16
|
def call(task)
|
|
17
|
-
raise DeprecationError,
|
|
17
|
+
raise DeprecationError, <<~MSG.chomp
|
|
18
|
+
#{task.class} is deprecated and prohibited from execution.
|
|
19
|
+
See https://drexed.github.io/cmdx/deprecation/#error
|
|
20
|
+
MSG
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
end
|
data/lib/cmdx/deprecators.rb
CHANGED
|
@@ -36,9 +36,12 @@ module CMDx
|
|
|
36
36
|
deprecator = callable || block
|
|
37
37
|
|
|
38
38
|
if callable && block
|
|
39
|
-
raise ArgumentError, "provide either a callable or a block, not both"
|
|
39
|
+
raise ArgumentError, "deprecator: provide either a callable or a block, not both"
|
|
40
40
|
elsif !deprecator.respond_to?(:call)
|
|
41
|
-
raise ArgumentError,
|
|
41
|
+
raise ArgumentError, <<~MSG.chomp
|
|
42
|
+
deprecator must respond to #call (got #{deprecator.class}).
|
|
43
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
44
|
+
MSG
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
registry[name.to_sym] = deprecator
|
|
@@ -48,22 +51,25 @@ module CMDx
|
|
|
48
51
|
# @param name [Symbol]
|
|
49
52
|
# @return [Deprecators] self for chaining
|
|
50
53
|
def deregister(name)
|
|
51
|
-
registry.delete(name
|
|
54
|
+
registry.delete(name)
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
# @param name [Symbol]
|
|
56
59
|
# @return [Boolean] whether a deprecator is registered under `name`
|
|
57
60
|
def key?(name)
|
|
58
|
-
registry.key?(name
|
|
61
|
+
registry.key?(name)
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
# @param name [Symbol]
|
|
62
65
|
# @return [#call] the registered deprecator
|
|
63
|
-
# @raise [
|
|
66
|
+
# @raise [UnknownEntryError] when `name` isn't registered
|
|
64
67
|
def lookup(name)
|
|
65
68
|
registry[name] || begin
|
|
66
|
-
raise
|
|
69
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
70
|
+
unknown deprecator #{name.inspect}; registered: #{registry.keys.inspect}.
|
|
71
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
72
|
+
MSG
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
|
|
@@ -73,7 +79,7 @@ module CMDx
|
|
|
73
79
|
#
|
|
74
80
|
# @param spec [Symbol, #call, nil]
|
|
75
81
|
# @return [#call, nil]
|
|
76
|
-
# @raise [
|
|
82
|
+
# @raise [UnknownEntryError] when `spec` is an unknown symbol or not callable
|
|
77
83
|
def resolve(spec)
|
|
78
84
|
case spec
|
|
79
85
|
when NilClass
|
|
@@ -83,7 +89,10 @@ module CMDx
|
|
|
83
89
|
else
|
|
84
90
|
return spec if spec.respond_to?(:call)
|
|
85
91
|
|
|
86
|
-
raise
|
|
92
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
93
|
+
unknown deprecator #{spec.inspect}; expected a Symbol from #{registry.keys.inspect} or a callable.
|
|
94
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
95
|
+
MSG
|
|
87
96
|
end
|
|
88
97
|
end
|
|
89
98
|
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -15,12 +15,13 @@ module CMDx
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Adds `message` under `key`. Duplicate messages are silently dropped.
|
|
18
|
+
# `key` is coerced to a Symbol to match {Context}'s key normalization.
|
|
18
19
|
#
|
|
19
|
-
# @param key [Symbol]
|
|
20
|
+
# @param key [Symbol, String]
|
|
20
21
|
# @param message [String]
|
|
21
22
|
# @return [Set<String>] the set of messages now stored under `key`
|
|
22
23
|
def add(key, message)
|
|
23
|
-
(messages[key] ||= Set.new) << message
|
|
24
|
+
(messages[key.to_sym] ||= Set.new) << message
|
|
24
25
|
end
|
|
25
26
|
alias []= add
|
|
26
27
|
|
|
@@ -40,23 +41,23 @@ module CMDx
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# @param key [Symbol]
|
|
44
|
+
# @param key [Symbol, String]
|
|
44
45
|
# @return [Array<String>] messages for `key`, or a frozen empty array
|
|
45
46
|
def [](key)
|
|
46
|
-
messages[key]&.to_a || EMPTY_ARRAY
|
|
47
|
+
messages[key.to_sym]&.to_a || EMPTY_ARRAY
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
# @param key [Symbol]
|
|
50
|
+
# @param key [Symbol, String]
|
|
50
51
|
# @param message [String]
|
|
51
52
|
# @return [Boolean] true when `message` is recorded under `key`
|
|
52
53
|
def added?(key, message)
|
|
53
|
-
!!messages[key]&.include?(message)
|
|
54
|
+
!!messages[key.to_sym]&.include?(message)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
# @param key [Symbol]
|
|
57
|
+
# @param key [Symbol, String]
|
|
57
58
|
# @return [Boolean]
|
|
58
59
|
def key?(key)
|
|
59
|
-
messages.key?(key)
|
|
60
|
+
messages.key?(key.to_sym)
|
|
60
61
|
end
|
|
61
62
|
alias for? key?
|
|
62
63
|
|
|
@@ -98,10 +99,10 @@ module CMDx
|
|
|
98
99
|
messages.each_value(&)
|
|
99
100
|
end
|
|
100
101
|
|
|
101
|
-
# @param key [Symbol]
|
|
102
|
+
# @param key [Symbol, String]
|
|
102
103
|
# @return [Set<String>, nil] the removed set, or nil when absent
|
|
103
104
|
def delete(key)
|
|
104
|
-
messages.delete(key)
|
|
105
|
+
messages.delete(key.to_sym)
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
# @return [Hash{Symbol => Set<String>}] empties the container
|
|
@@ -113,7 +114,10 @@ module CMDx
|
|
|
113
114
|
# (e.g. `{ name: ["name is required"] }`)
|
|
114
115
|
def full_messages
|
|
115
116
|
messages.each_with_object({}) do |(key, set), hash|
|
|
116
|
-
hash[key] = set.map
|
|
117
|
+
hash[key] = set.map do |message|
|
|
118
|
+
i18n_message = I18nProxy.t(message, default: message)
|
|
119
|
+
"#{key} #{i18n_message}"
|
|
120
|
+
end
|
|
117
121
|
end
|
|
118
122
|
end
|
|
119
123
|
|
data/lib/cmdx/executors/fiber.rb
CHANGED
|
@@ -5,7 +5,8 @@ module CMDx
|
|
|
5
5
|
# Fiber-scheduler backed executor. Spawns one fiber per job, bounded by
|
|
6
6
|
# `concurrency` via a `SizedQueue` semaphore. Requires a Fiber scheduler to
|
|
7
7
|
# be installed on the current thread (e.g. inside `Async { ... }` from the
|
|
8
|
-
# `async` gem). `pool_size` caps in-flight fibers.
|
|
8
|
+
# `async` gem). `pool_size` caps in-flight fibers. Exceptions raised inside
|
|
9
|
+
# `on_job` are captured and re-raised once every fiber has completed.
|
|
9
10
|
#
|
|
10
11
|
# @api private
|
|
11
12
|
module Fiber
|
|
@@ -13,21 +14,30 @@ module CMDx
|
|
|
13
14
|
extend self
|
|
14
15
|
|
|
15
16
|
# @param jobs [Array]
|
|
16
|
-
# @param concurrency [Integer] max in-flight fibers
|
|
17
|
+
# @param concurrency [Integer] max in-flight fibers (must be >= 1)
|
|
17
18
|
# @param on_job [#call]
|
|
18
19
|
# @return [void]
|
|
20
|
+
# @raise [ArgumentError] when `concurrency` is not a positive Integer
|
|
19
21
|
# @raise [RuntimeError] when no `Fiber.scheduler` is installed
|
|
22
|
+
# @raise [StandardError] re-raises the first exception captured from any fiber
|
|
20
23
|
def call(jobs:, concurrency:, on_job:)
|
|
24
|
+
raise ArgumentError, "executor concurrency must be a positive Integer (got #{concurrency.inspect})" unless concurrency.is_a?(Integer) && concurrency.positive?
|
|
25
|
+
|
|
21
26
|
raise "executor: :fibers requires Fiber.scheduler; run the workflow inside a scheduler block (e.g. Async { ... })" unless ::Fiber.scheduler
|
|
22
27
|
|
|
23
28
|
slots = SizedQueue.new(concurrency)
|
|
24
29
|
concurrency.times { slots << :slot }
|
|
25
|
-
done
|
|
30
|
+
done = Queue.new
|
|
31
|
+
errors = Queue.new
|
|
26
32
|
|
|
27
33
|
jobs.each do |job|
|
|
28
34
|
slots.pop
|
|
29
35
|
::Fiber.schedule do
|
|
30
|
-
|
|
36
|
+
begin
|
|
37
|
+
on_job.call(job)
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
errors << e
|
|
40
|
+
end
|
|
31
41
|
ensure
|
|
32
42
|
slots << :slot
|
|
33
43
|
done << true
|
|
@@ -35,6 +45,8 @@ module CMDx
|
|
|
35
45
|
end
|
|
36
46
|
|
|
37
47
|
jobs.size.times { done.pop }
|
|
48
|
+
|
|
49
|
+
raise errors.pop unless errors.empty?
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
end
|
|
@@ -4,7 +4,9 @@ module CMDx
|
|
|
4
4
|
class Executors
|
|
5
5
|
# Default executor. Uses a fixed-size `Thread` pool drained via a `Queue`;
|
|
6
6
|
# sentinel `nil`s terminate workers. Workers inherit the parent's chain via
|
|
7
|
-
# fiber-local storage.
|
|
7
|
+
# fiber-local storage. Exceptions raised inside `on_job` are captured per
|
|
8
|
+
# worker and re-raised on the main thread once every worker has joined, so
|
|
9
|
+
# callers see failures instead of silently dropped jobs.
|
|
8
10
|
#
|
|
9
11
|
# @api private
|
|
10
12
|
module Thread
|
|
@@ -12,23 +14,35 @@ module CMDx
|
|
|
12
14
|
extend self
|
|
13
15
|
|
|
14
16
|
# @param jobs [Array] opaque job objects forwarded to `on_job`
|
|
15
|
-
# @param concurrency [Integer] worker count
|
|
17
|
+
# @param concurrency [Integer] worker count (must be >= 1)
|
|
16
18
|
# @param on_job [#call] unary callable invoked per job
|
|
17
19
|
# @return [void]
|
|
20
|
+
# @raise [ArgumentError] when `concurrency` is not a positive Integer
|
|
21
|
+
# @raise [StandardError] re-raises the first exception captured from any worker
|
|
18
22
|
def call(jobs:, concurrency:, on_job:)
|
|
19
|
-
|
|
23
|
+
raise ArgumentError, "executor concurrency must be a positive Integer (got #{concurrency.inspect})" unless concurrency.is_a?(Integer) && concurrency.positive?
|
|
24
|
+
|
|
25
|
+
queue = Queue.new
|
|
26
|
+
errors = Queue.new
|
|
27
|
+
|
|
20
28
|
jobs.each { |job| queue << job }
|
|
21
29
|
concurrency.times { queue << nil }
|
|
22
30
|
|
|
23
31
|
workers = Array.new(concurrency) do
|
|
24
32
|
::Thread.new do
|
|
25
33
|
while (job = queue.pop)
|
|
26
|
-
|
|
34
|
+
begin
|
|
35
|
+
on_job.call(job)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
errors << e
|
|
38
|
+
end
|
|
27
39
|
end
|
|
28
40
|
end
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
workers.each(&:join)
|
|
44
|
+
|
|
45
|
+
raise errors.pop unless errors.empty?
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
end
|
data/lib/cmdx/executors.rb
CHANGED
|
@@ -36,9 +36,12 @@ module CMDx
|
|
|
36
36
|
executor = callable || block
|
|
37
37
|
|
|
38
38
|
if callable && block
|
|
39
|
-
raise ArgumentError, "provide either a callable or a block, not both"
|
|
39
|
+
raise ArgumentError, "executor: provide either a callable or a block, not both"
|
|
40
40
|
elsif !executor.respond_to?(:call)
|
|
41
|
-
raise ArgumentError,
|
|
41
|
+
raise ArgumentError, <<~MSG.chomp
|
|
42
|
+
executor must respond to #call (got #{executor.class}).
|
|
43
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
44
|
+
MSG
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
registry[name.to_sym] = executor
|
|
@@ -48,16 +51,25 @@ module CMDx
|
|
|
48
51
|
# @param name [Symbol]
|
|
49
52
|
# @return [Executors] self for chaining
|
|
50
53
|
def deregister(name)
|
|
51
|
-
registry.delete(name
|
|
54
|
+
registry.delete(name)
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
# @param name [Symbol]
|
|
59
|
+
# @return [Boolean] whether an executor is registered under `name`
|
|
60
|
+
def key?(name)
|
|
61
|
+
registry.key?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
# @param name [Symbol]
|
|
56
65
|
# @return [#call] the registered executor
|
|
57
|
-
# @raise [
|
|
66
|
+
# @raise [UnknownEntryError] when `name` isn't registered
|
|
58
67
|
def lookup(name)
|
|
59
68
|
registry[name] || begin
|
|
60
|
-
raise
|
|
69
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
70
|
+
unknown executor #{name.inspect}; registered: #{registry.keys.inspect}.
|
|
71
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
72
|
+
MSG
|
|
61
73
|
end
|
|
62
74
|
end
|
|
63
75
|
|
|
@@ -67,7 +79,7 @@ module CMDx
|
|
|
67
79
|
#
|
|
68
80
|
# @param spec [Symbol, #call, nil]
|
|
69
81
|
# @return [#call]
|
|
70
|
-
# @raise [
|
|
82
|
+
# @raise [UnknownEntryError] when `spec` is an unknown symbol or not callable
|
|
71
83
|
def resolve(spec)
|
|
72
84
|
case spec
|
|
73
85
|
when NilClass
|
|
@@ -77,7 +89,10 @@ module CMDx
|
|
|
77
89
|
else
|
|
78
90
|
return spec if spec.respond_to?(:call)
|
|
79
91
|
|
|
80
|
-
raise
|
|
92
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
93
|
+
unknown executor #{spec.inspect}; expected a Symbol from #{registry.keys.inspect} or a callable.
|
|
94
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
95
|
+
MSG
|
|
81
96
|
end
|
|
82
97
|
end
|
|
83
98
|
|
data/lib/cmdx/fault.rb
CHANGED
|
@@ -16,6 +16,10 @@ module CMDx
|
|
|
16
16
|
# inherits from) any of the given task classes. Suitable for use in
|
|
17
17
|
# `rescue`.
|
|
18
18
|
#
|
|
19
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
20
|
+
# to module scope (`MY_FAULT = CMDx::Fault.for?(MyTask)`) when used
|
|
21
|
+
# on hot paths to avoid class allocation churn over time.
|
|
22
|
+
#
|
|
19
23
|
# @param tasks [Array<Class>] one or more Task classes
|
|
20
24
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
21
25
|
# @raise [ArgumentError] when no tasks are given
|
|
@@ -28,7 +32,7 @@ module CMDx
|
|
|
28
32
|
# end
|
|
29
33
|
def for?(*tasks)
|
|
30
34
|
tasks = tasks.flatten
|
|
31
|
-
raise ArgumentError, "at least one
|
|
35
|
+
raise ArgumentError, "Fault.for? requires at least one Task class" if tasks.empty?
|
|
32
36
|
|
|
33
37
|
matcher do |other|
|
|
34
38
|
tasks.any? { |task| other.task <= task }
|
|
@@ -38,6 +42,10 @@ module CMDx
|
|
|
38
42
|
# Returns a matcher subclass that matches Faults whose `result.reason`
|
|
39
43
|
# is equal to the given string. Suitable for use in `rescue`.
|
|
40
44
|
#
|
|
45
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
46
|
+
# to module scope (`PAYMENT_FAULT = CMDx::Fault.reason?("Payment failed")`)
|
|
47
|
+
# when used on hot paths to avoid class allocation churn.
|
|
48
|
+
#
|
|
41
49
|
# @param reason [String] the reason to match
|
|
42
50
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
43
51
|
# @raise [ArgumentError] when no reason is given
|
|
@@ -49,7 +57,7 @@ module CMDx
|
|
|
49
57
|
# Alert.for_fault(fault)
|
|
50
58
|
# end
|
|
51
59
|
def reason?(reason)
|
|
52
|
-
raise ArgumentError, "reason
|
|
60
|
+
raise ArgumentError, "Fault.reason? requires a reason" unless reason
|
|
53
61
|
|
|
54
62
|
matcher do |other|
|
|
55
63
|
other.result.reason == reason
|
|
@@ -58,13 +66,17 @@ module CMDx
|
|
|
58
66
|
|
|
59
67
|
# Returns a matcher subclass whose `===` runs `block` against the fault.
|
|
60
68
|
#
|
|
69
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
70
|
+
# to module scope when used on hot paths to avoid class allocation
|
|
71
|
+
# churn.
|
|
72
|
+
#
|
|
61
73
|
# @param block [#call] `(fault) -> Boolean` matcher body
|
|
62
74
|
# @yieldparam fault [Fault]
|
|
63
75
|
# @yieldreturn [Boolean]
|
|
64
76
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
65
77
|
# @raise [ArgumentError] when no block is given
|
|
66
78
|
def matches?(&block)
|
|
67
|
-
raise ArgumentError, "block
|
|
79
|
+
raise ArgumentError, "Fault.matches? requires a block" unless block
|
|
68
80
|
|
|
69
81
|
matcher(&block)
|
|
70
82
|
end
|
data/lib/cmdx/i18n_proxy.rb
CHANGED
|
@@ -62,7 +62,7 @@ module CMDx
|
|
|
62
62
|
# @option options [Hash{Symbol => Object}] extra keys interpolated via `String#%` for bundled translations
|
|
63
63
|
# @return [String, Object] the translated/interpolated message
|
|
64
64
|
def translate(key, **options)
|
|
65
|
-
return ::I18n.translate(key, **options) if defined?(::I18n)
|
|
65
|
+
return ::I18n.translate(key, **options) if defined?(::I18n)
|
|
66
66
|
|
|
67
67
|
message = translation_default(key) || options[:default]
|
|
68
68
|
|
|
@@ -79,8 +79,6 @@ module CMDx
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
-
# @param key [String, Symbol] lookup key (without locale prefix)
|
|
83
|
-
# @return [Object, nil] message template from bundled YAML, when present
|
|
84
82
|
def translation_default(key)
|
|
85
83
|
default_locale = CMDx.configuration.default_locale || "en"
|
|
86
84
|
translation_key = "#{default_locale}.#{key}"
|
|
@@ -92,9 +90,15 @@ module CMDx
|
|
|
92
90
|
@translations[default_locale] ||= begin
|
|
93
91
|
file = "#{default_locale}.yml"
|
|
94
92
|
paths = self.class.locale_paths.map { |dir| File.join(dir, file) }.select { |p| File.exist?(p) }
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if paths.empty?
|
|
94
|
+
raise UnknownLocaleError, <<~MSG.chomp
|
|
95
|
+
unable to load #{default_locale.inspect} translations;
|
|
96
|
+
searched: #{self.class.locale_paths.map { |dir| File.join(dir, file) }.inspect}.
|
|
97
|
+
See https://drexed.github.io/cmdx/internationalization/#custom-locale-paths
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
paths.reduce({}) { |acc, path| Util.deep_merge(acc, YAML.safe_load_file(path)) }.freeze
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
@defaults[translation_key] = @translations[default_locale].dig(*translation_key.split("."))
|