cmdx 1.18.0 → 1.20.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/.DS_Store +0 -0
- data/CHANGELOG.md +73 -9
- data/README.md +1 -1
- data/lib/cmdx/attribute.rb +88 -20
- data/lib/cmdx/attribute_registry.rb +79 -8
- data/lib/cmdx/attribute_value.rb +8 -3
- data/lib/cmdx/callback_registry.rb +60 -26
- data/lib/cmdx/chain.rb +47 -4
- data/lib/cmdx/coercion_registry.rb +42 -20
- data/lib/cmdx/coercions/array.rb +8 -3
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +6 -2
- data/lib/cmdx/coercions/complex.rb +1 -1
- data/lib/cmdx/coercions/date.rb +2 -7
- data/lib/cmdx/coercions/date_time.rb +2 -7
- data/lib/cmdx/coercions/float.rb +1 -1
- data/lib/cmdx/coercions/hash.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +4 -5
- data/lib/cmdx/coercions/rational.rb +1 -1
- data/lib/cmdx/coercions/string.rb +1 -1
- data/lib/cmdx/coercions/symbol.rb +1 -1
- data/lib/cmdx/coercions/time.rb +1 -7
- data/lib/cmdx/configuration.rb +26 -0
- data/lib/cmdx/context.rb +9 -6
- data/lib/cmdx/deprecator.rb +27 -14
- data/lib/cmdx/errors.rb +3 -4
- data/lib/cmdx/exception.rb +7 -0
- data/lib/cmdx/executor.rb +77 -54
- data/lib/cmdx/identifier.rb +4 -6
- data/lib/cmdx/locale.rb +32 -9
- data/lib/cmdx/middleware_registry.rb +43 -23
- data/lib/cmdx/middlewares/correlate.rb +4 -2
- data/lib/cmdx/middlewares/timeout.rb +11 -10
- data/lib/cmdx/parallelizer.rb +100 -0
- data/lib/cmdx/pipeline.rb +42 -23
- data/lib/cmdx/railtie.rb +1 -1
- data/lib/cmdx/result.rb +27 -11
- data/lib/cmdx/retry.rb +166 -0
- data/lib/cmdx/settings.rb +222 -0
- data/lib/cmdx/task.rb +53 -61
- data/lib/cmdx/utils/format.rb +17 -1
- data/lib/cmdx/utils/normalize.rb +52 -0
- data/lib/cmdx/utils/wrap.rb +38 -0
- data/lib/cmdx/validator_registry.rb +45 -20
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +2 -2
- data/lib/cmdx/validators/format.rb +1 -1
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +1 -1
- data/lib/cmdx/validators/numeric.rb +1 -1
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +12 -0
- data/lib/generators/cmdx/templates/install.rb +11 -0
- data/mkdocs.yml +5 -1
- metadata +6 -15
data/lib/cmdx/executor.rb
CHANGED
|
@@ -10,6 +10,14 @@ module CMDx
|
|
|
10
10
|
|
|
11
11
|
extend Forwardable
|
|
12
12
|
|
|
13
|
+
# @rbs STATE_CALLBACKS: Hash[String, Symbol]
|
|
14
|
+
STATE_CALLBACKS = Result::STATES.to_h { |s| [s, :"on_#{s}"] }.freeze
|
|
15
|
+
private_constant :STATE_CALLBACKS
|
|
16
|
+
|
|
17
|
+
# @rbs STATUS_CALLBACKS: Hash[String, Symbol]
|
|
18
|
+
STATUS_CALLBACKS = Result::STATUSES.to_h { |s| [s, :"on_#{s}"] }.freeze
|
|
19
|
+
private_constant :STATUS_CALLBACKS
|
|
20
|
+
|
|
13
21
|
# Returns the task being executed.
|
|
14
22
|
#
|
|
15
23
|
# @return [Task] The task instance
|
|
@@ -63,23 +71,24 @@ module CMDx
|
|
|
63
71
|
#
|
|
64
72
|
# @rbs () -> Result
|
|
65
73
|
def execute
|
|
66
|
-
task.class.settings
|
|
74
|
+
task.class.settings.middlewares.call!(task) do
|
|
67
75
|
pre_execution! unless @pre_execution
|
|
68
76
|
execution!
|
|
69
|
-
|
|
77
|
+
verify_context_returns!
|
|
70
78
|
rescue UndefinedMethodError => e
|
|
71
|
-
|
|
79
|
+
raise_exception(e)
|
|
72
80
|
rescue Fault => e
|
|
73
81
|
result.throw!(e.result, halt: false, cause: e)
|
|
74
82
|
rescue StandardError => e
|
|
75
83
|
retry if retry_execution?(e)
|
|
76
|
-
result.fail!(
|
|
77
|
-
task.class.settings
|
|
84
|
+
result.fail!(Utils::Normalize.exception(e), halt: false, cause: e, source: :exception)
|
|
85
|
+
task.class.settings.exception_handler&.call(task, e)
|
|
78
86
|
ensure
|
|
79
87
|
result.executed!
|
|
80
88
|
post_execution!
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
verify_middleware_yield!
|
|
83
92
|
finalize_execution!
|
|
84
93
|
end
|
|
85
94
|
|
|
@@ -95,24 +104,31 @@ module CMDx
|
|
|
95
104
|
#
|
|
96
105
|
# @rbs () -> Result
|
|
97
106
|
def execute!
|
|
98
|
-
task.class.settings
|
|
107
|
+
task.class.settings.middlewares.call!(task) do
|
|
99
108
|
pre_execution! unless @pre_execution
|
|
100
109
|
execution!
|
|
101
|
-
|
|
110
|
+
verify_context_returns!
|
|
102
111
|
rescue UndefinedMethodError => e
|
|
103
112
|
raise_exception(e)
|
|
104
113
|
rescue Fault => e
|
|
105
114
|
result.throw!(e.result, halt: false, cause: e)
|
|
106
|
-
|
|
115
|
+
|
|
116
|
+
if halt_execution?(e)
|
|
117
|
+
raise_exception(e)
|
|
118
|
+
else
|
|
119
|
+
result.executed!
|
|
120
|
+
post_execution!
|
|
121
|
+
end
|
|
107
122
|
rescue StandardError => e
|
|
108
123
|
retry if retry_execution?(e)
|
|
109
|
-
result.fail!(
|
|
124
|
+
result.fail!(Utils::Normalize.exception(e), halt: false, cause: e, source: :exception)
|
|
110
125
|
raise_exception(e)
|
|
111
126
|
else
|
|
112
127
|
result.executed!
|
|
113
128
|
post_execution!
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
verify_middleware_yield!
|
|
116
132
|
finalize_execution!
|
|
117
133
|
end
|
|
118
134
|
|
|
@@ -126,10 +142,12 @@ module CMDx
|
|
|
126
142
|
#
|
|
127
143
|
# @rbs (Exception exception) -> bool
|
|
128
144
|
def halt_execution?(exception)
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
@halt_statuses ||= Utils::Normalize.statuses(
|
|
146
|
+
task.class.settings.breakpoints ||
|
|
147
|
+
task.class.settings.task_breakpoints
|
|
148
|
+
).freeze
|
|
131
149
|
|
|
132
|
-
|
|
150
|
+
@halt_statuses.include?(exception.result.status)
|
|
133
151
|
end
|
|
134
152
|
|
|
135
153
|
# Determines if execution should be retried based on retry configuration.
|
|
@@ -140,36 +158,22 @@ module CMDx
|
|
|
140
158
|
#
|
|
141
159
|
# @rbs (Exception exception) -> bool
|
|
142
160
|
def retry_execution?(exception)
|
|
143
|
-
|
|
144
|
-
return false unless available_retries.positive?
|
|
145
|
-
|
|
146
|
-
current_retry = result.retries
|
|
147
|
-
remaining_retries = available_retries - current_retry
|
|
148
|
-
return false unless remaining_retries.positive?
|
|
161
|
+
@retry ||= Retry.new(task)
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
return false unless
|
|
163
|
+
return false unless @retry.available? && @retry.remaining?
|
|
164
|
+
return false unless @retry.exception?(exception)
|
|
152
165
|
|
|
153
166
|
result.retries += 1
|
|
154
167
|
|
|
155
168
|
task.logger.warn do
|
|
156
|
-
reason =
|
|
157
|
-
task.to_h.merge!(reason:, remaining_retries:)
|
|
169
|
+
reason = Utils::Normalize.exception(exception)
|
|
170
|
+
task.to_h.merge!(reason:, remaining_retries: @retry.remaining)
|
|
158
171
|
end
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
jitter =
|
|
162
|
-
if jitter.is_a?(Symbol)
|
|
163
|
-
task.send(jitter, current_retry)
|
|
164
|
-
elsif jitter.is_a?(Proc)
|
|
165
|
-
task.instance_exec(current_retry, &jitter)
|
|
166
|
-
elsif jitter.respond_to?(:call)
|
|
167
|
-
jitter.call(task, current_retry)
|
|
168
|
-
else
|
|
169
|
-
jitter.to_f * current_retry
|
|
170
|
-
end
|
|
173
|
+
task.errors.clear
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
wait = @retry.wait
|
|
176
|
+
sleep(wait) if wait.positive?
|
|
173
177
|
|
|
174
178
|
true
|
|
175
179
|
end
|
|
@@ -198,7 +202,7 @@ module CMDx
|
|
|
198
202
|
#
|
|
199
203
|
# @rbs (Symbol type) -> void
|
|
200
204
|
def invoke_callbacks(type)
|
|
201
|
-
task.class.settings
|
|
205
|
+
task.class.settings.callbacks.invoke(type, task)
|
|
202
206
|
end
|
|
203
207
|
|
|
204
208
|
private
|
|
@@ -211,11 +215,12 @@ module CMDx
|
|
|
211
215
|
|
|
212
216
|
invoke_callbacks(:before_validation)
|
|
213
217
|
|
|
214
|
-
task.class.settings
|
|
218
|
+
task.class.settings.attributes.define_and_verify(task)
|
|
215
219
|
return if task.errors.empty?
|
|
216
220
|
|
|
217
221
|
result.fail!(
|
|
218
222
|
Locale.t("cmdx.faults.invalid"),
|
|
223
|
+
source: :validation,
|
|
219
224
|
errors: {
|
|
220
225
|
full_message: task.errors.to_s,
|
|
221
226
|
messages: task.errors.to_h
|
|
@@ -236,10 +241,10 @@ module CMDx
|
|
|
236
241
|
# Verifies that all declared returns are present in the context after execution.
|
|
237
242
|
#
|
|
238
243
|
# @rbs () -> void
|
|
239
|
-
def
|
|
244
|
+
def verify_context_returns!
|
|
240
245
|
return unless result.success?
|
|
241
246
|
|
|
242
|
-
returns =
|
|
247
|
+
returns = Utils::Wrap.array(task.class.settings.returns)
|
|
243
248
|
missing = returns.reject { |name| task.context.key?(name) }
|
|
244
249
|
return if missing.empty?
|
|
245
250
|
|
|
@@ -247,6 +252,7 @@ module CMDx
|
|
|
247
252
|
|
|
248
253
|
result.fail!(
|
|
249
254
|
Locale.t("cmdx.faults.invalid"),
|
|
255
|
+
source: :context,
|
|
250
256
|
errors: {
|
|
251
257
|
full_message: task.errors.to_s,
|
|
252
258
|
messages: task.errors.to_h
|
|
@@ -258,20 +264,37 @@ module CMDx
|
|
|
258
264
|
#
|
|
259
265
|
# @rbs () -> void
|
|
260
266
|
def post_execution!
|
|
261
|
-
|
|
267
|
+
return if task.class.settings.callbacks.empty?
|
|
268
|
+
|
|
269
|
+
invoke_callbacks(STATE_CALLBACKS[result.state])
|
|
262
270
|
invoke_callbacks(:on_executed) if result.executed?
|
|
263
271
|
|
|
264
|
-
invoke_callbacks(
|
|
272
|
+
invoke_callbacks(STATUS_CALLBACKS[result.status])
|
|
265
273
|
invoke_callbacks(:on_good) if result.good?
|
|
266
274
|
invoke_callbacks(:on_bad) if result.bad?
|
|
267
275
|
end
|
|
268
276
|
|
|
277
|
+
# Detects if middleware swallowed the block without yielding.
|
|
278
|
+
# When this happens the result is still in the initialized state.
|
|
279
|
+
#
|
|
280
|
+
# @rbs () -> void
|
|
281
|
+
def verify_middleware_yield!
|
|
282
|
+
return unless result.initialized?
|
|
283
|
+
|
|
284
|
+
result.fail!(
|
|
285
|
+
Locale.t("cmdx.faults.invalid"),
|
|
286
|
+
halt: false,
|
|
287
|
+
source: :middleware
|
|
288
|
+
)
|
|
289
|
+
result.executed!
|
|
290
|
+
end
|
|
291
|
+
|
|
269
292
|
# Finalizes execution by freezing the task, logging results, and rolling back work.
|
|
270
293
|
#
|
|
271
|
-
# @rbs () ->
|
|
294
|
+
# @rbs () -> void
|
|
272
295
|
def finalize_execution!
|
|
273
296
|
log_execution!
|
|
274
|
-
log_backtrace! if task.class.settings
|
|
297
|
+
log_backtrace! if task.class.settings.backtrace
|
|
275
298
|
|
|
276
299
|
rollback_execution!
|
|
277
300
|
freeze_execution!
|
|
@@ -291,12 +314,12 @@ module CMDx
|
|
|
291
314
|
def log_backtrace!
|
|
292
315
|
return unless result.failed?
|
|
293
316
|
|
|
294
|
-
exception = result.caused_failure
|
|
295
|
-
return if exception.is_a?(Fault)
|
|
317
|
+
exception = result.caused_failure&.cause
|
|
318
|
+
return if exception.nil? || exception.is_a?(Fault)
|
|
296
319
|
|
|
297
320
|
task.logger.error do
|
|
298
|
-
|
|
299
|
-
if (cleaner = task.class.settings
|
|
321
|
+
Utils::Normalize.exception(exception) << "\n" <<
|
|
322
|
+
if (cleaner = task.class.settings.backtrace_cleaner)
|
|
300
323
|
cleaner.call(exception.backtrace).join("\n\t")
|
|
301
324
|
else
|
|
302
325
|
exception.full_message(highlight: false)
|
|
@@ -309,25 +332,26 @@ module CMDx
|
|
|
309
332
|
# @rbs () -> void
|
|
310
333
|
def freeze_execution!
|
|
311
334
|
# Stubbing on frozen objects is not allowed in most test environments.
|
|
312
|
-
|
|
313
|
-
return if Coercions::Boolean.call(skip_freezing)
|
|
335
|
+
return unless CMDx.configuration.freeze_results
|
|
314
336
|
|
|
315
337
|
task.freeze
|
|
316
338
|
result.freeze
|
|
317
339
|
|
|
318
|
-
# Freezing the context and chain can only be done
|
|
319
|
-
#
|
|
340
|
+
# Freezing the context and chain can only be done once the outer-most
|
|
341
|
+
# task has completed.
|
|
320
342
|
return unless result.index.zero?
|
|
321
343
|
|
|
322
344
|
task.context.freeze
|
|
323
345
|
task.chain.freeze
|
|
324
346
|
end
|
|
325
347
|
|
|
326
|
-
# Clears the chain if the task is the outermost (top-level) task
|
|
348
|
+
# Clears the chain if the task is the outermost (top-level) task
|
|
349
|
+
# and the current thread's chain is the same instance this task belongs to.
|
|
327
350
|
#
|
|
328
351
|
# @rbs () -> void
|
|
329
352
|
def clear_chain!
|
|
330
353
|
return unless result.index.zero?
|
|
354
|
+
return unless Chain.current.equal?(task.chain)
|
|
331
355
|
|
|
332
356
|
Chain.clear
|
|
333
357
|
end
|
|
@@ -339,9 +363,8 @@ module CMDx
|
|
|
339
363
|
return if result.rolled_back?
|
|
340
364
|
return unless task.respond_to?(:rollback)
|
|
341
365
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return unless statuses.include?(result.status)
|
|
366
|
+
@rollback_statuses ||= Utils::Normalize.statuses(task.class.settings.rollback_on).freeze
|
|
367
|
+
return unless @rollback_statuses.include?(result.status)
|
|
345
368
|
|
|
346
369
|
result.rolled_back = true
|
|
347
370
|
task.rollback
|
data/lib/cmdx/identifier.rb
CHANGED
|
@@ -20,12 +20,10 @@ module CMDx
|
|
|
20
20
|
# # => "01890b2c-1234-5678-9abc-def123456789"
|
|
21
21
|
#
|
|
22
22
|
# @rbs () -> String
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
SecureRandom.uuid
|
|
28
|
-
end
|
|
23
|
+
if SecureRandom.respond_to?(:uuid_v7)
|
|
24
|
+
def generate = SecureRandom.uuid_v7
|
|
25
|
+
else
|
|
26
|
+
def generate = SecureRandom.uuid
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
end
|
data/lib/cmdx/locale.rb
CHANGED
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
# Provides internationalization and localization support for CMDx.
|
|
5
|
-
# Handles translation lookups with fallback to
|
|
6
|
-
# when I18n gem is not available.
|
|
5
|
+
# Handles translation lookups with fallback to the configured locale's
|
|
6
|
+
# default messages when I18n gem is not available.
|
|
7
7
|
module Locale
|
|
8
8
|
|
|
9
9
|
extend self
|
|
10
10
|
|
|
11
|
-
# @rbs EN: Hash[String, untyped]
|
|
12
|
-
EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze
|
|
13
|
-
private_constant :EN
|
|
14
|
-
|
|
15
11
|
# Translates a key to the current locale with optional interpolation.
|
|
16
|
-
# Falls back to
|
|
12
|
+
# Falls back to the configured locale's YAML file translations if
|
|
13
|
+
# I18n gem is unavailable.
|
|
17
14
|
#
|
|
18
15
|
# @param key [String, Symbol] The translation key (supports dot notation)
|
|
19
16
|
# @param options [Hash] Translation options
|
|
@@ -38,12 +35,12 @@ module CMDx
|
|
|
38
35
|
#
|
|
39
36
|
# @rbs ((String | Symbol) key, **untyped options) -> String
|
|
40
37
|
def translate(key, **options)
|
|
41
|
-
options[:default] ||=
|
|
38
|
+
options[:default] ||= translation_default(key)
|
|
42
39
|
return ::I18n.t(key, **options) if defined?(::I18n)
|
|
43
40
|
|
|
44
41
|
case message = options.delete(:default)
|
|
45
|
-
when NilClass then "Translation missing: #{key}"
|
|
46
42
|
when String then message % options
|
|
43
|
+
when NilClass then "Translation missing: #{key}"
|
|
47
44
|
else message
|
|
48
45
|
end
|
|
49
46
|
end
|
|
@@ -51,5 +48,31 @@ module CMDx
|
|
|
51
48
|
# @see #translate
|
|
52
49
|
alias t translate
|
|
53
50
|
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Resolves and caches the default translation for a key by digging
|
|
54
|
+
# into the configured locale's YAML translations.
|
|
55
|
+
#
|
|
56
|
+
# @param key [String, Symbol] The translation key
|
|
57
|
+
#
|
|
58
|
+
# @return [String, nil] The resolved translation or nil
|
|
59
|
+
#
|
|
60
|
+
# @rbs ((String | Symbol) key) -> String?
|
|
61
|
+
def translation_default(key)
|
|
62
|
+
tkey = "#{CMDx.configuration.default_locale}.#{key}"
|
|
63
|
+
|
|
64
|
+
@translation_defaults ||= {}
|
|
65
|
+
return @translation_defaults[tkey] if @translation_defaults.key?(tkey)
|
|
66
|
+
|
|
67
|
+
@default_translations ||= begin
|
|
68
|
+
path = CMDx.gem_path.join("lib/locales/#{CMDx.configuration.default_locale}.yml")
|
|
69
|
+
raise ArgumentError, "locale file not found: #{path}" unless path.exist?
|
|
70
|
+
|
|
71
|
+
YAML.load_file(path).freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@translation_defaults[tkey] = @default_translations.dig(*tkey.split("."))
|
|
75
|
+
end
|
|
76
|
+
|
|
54
77
|
end
|
|
55
78
|
end
|
|
@@ -7,43 +7,48 @@ module CMDx
|
|
|
7
7
|
# that can be inserted, removed, and executed in sequence. Each middleware
|
|
8
8
|
# can be configured with specific options and is executed in the order
|
|
9
9
|
# they were registered.
|
|
10
|
+
#
|
|
11
|
+
# Supports copy-on-write semantics: a duped registry shares the parent's
|
|
12
|
+
# data until a write operation triggers materialization.
|
|
10
13
|
class MiddlewareRegistry
|
|
11
14
|
|
|
12
|
-
# Returns the ordered collection of middleware entries.
|
|
13
|
-
#
|
|
14
|
-
# @return [Array<Array>] Array of middleware-options pairs
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]]
|
|
18
|
-
#
|
|
19
|
-
# @rbs @registry: Array[Array[untyped]]
|
|
20
|
-
attr_reader :registry
|
|
21
|
-
alias to_a registry
|
|
22
|
-
|
|
23
15
|
# Initialize a new middleware registry.
|
|
24
16
|
#
|
|
25
|
-
# @param registry [Array] Initial array of middleware entries
|
|
17
|
+
# @param registry [Array, nil] Initial array of middleware entries
|
|
26
18
|
#
|
|
27
19
|
# @example
|
|
28
20
|
# registry = MiddlewareRegistry.new
|
|
29
21
|
# registry = MiddlewareRegistry.new([[MyMiddleware, {option: 'value'}]])
|
|
30
22
|
#
|
|
31
|
-
# @rbs (?Array[Array[untyped]] registry) -> void
|
|
32
|
-
def initialize(registry =
|
|
33
|
-
@registry = registry
|
|
23
|
+
# @rbs (?Array[Array[untyped]]? registry) -> void
|
|
24
|
+
def initialize(registry = nil)
|
|
25
|
+
@registry = registry || []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sets up copy-on-write state when duplicated via dup.
|
|
29
|
+
#
|
|
30
|
+
# @param source [MiddlewareRegistry] The registry being duplicated
|
|
31
|
+
#
|
|
32
|
+
# @rbs (MiddlewareRegistry source) -> void
|
|
33
|
+
def initialize_dup(source)
|
|
34
|
+
@parent = source
|
|
35
|
+
@registry = nil
|
|
36
|
+
super
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
#
|
|
39
|
+
# Returns the ordered collection of middleware entries.
|
|
40
|
+
# Delegates to the parent registry when not yet materialized.
|
|
37
41
|
#
|
|
38
|
-
# @return [
|
|
42
|
+
# @return [Array<Array>] Array of middleware-options pairs
|
|
39
43
|
#
|
|
40
44
|
# @example
|
|
41
|
-
#
|
|
45
|
+
# registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]]
|
|
42
46
|
#
|
|
43
|
-
# @rbs () ->
|
|
44
|
-
def
|
|
45
|
-
|
|
47
|
+
# @rbs () -> Array[Array[untyped]]
|
|
48
|
+
def registry
|
|
49
|
+
@registry || @parent.registry
|
|
46
50
|
end
|
|
51
|
+
alias to_a registry
|
|
47
52
|
|
|
48
53
|
# Register a middleware component in the registry.
|
|
49
54
|
#
|
|
@@ -61,7 +66,9 @@ module CMDx
|
|
|
61
66
|
#
|
|
62
67
|
# @rbs (untyped middleware, ?at: Integer, **untyped options) -> self
|
|
63
68
|
def register(middleware, at: -1, **options)
|
|
64
|
-
|
|
69
|
+
materialize!
|
|
70
|
+
|
|
71
|
+
@registry.insert(at, [middleware, options])
|
|
65
72
|
self
|
|
66
73
|
end
|
|
67
74
|
|
|
@@ -76,7 +83,9 @@ module CMDx
|
|
|
76
83
|
#
|
|
77
84
|
# @rbs (untyped middleware) -> self
|
|
78
85
|
def deregister(middleware)
|
|
79
|
-
|
|
86
|
+
materialize!
|
|
87
|
+
|
|
88
|
+
@registry.reject! { |mw, _opts| mw == middleware }
|
|
80
89
|
self
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -105,6 +114,17 @@ module CMDx
|
|
|
105
114
|
|
|
106
115
|
private
|
|
107
116
|
|
|
117
|
+
# Copies the parent's registry data into this instance,
|
|
118
|
+
# severing the copy-on-write link.
|
|
119
|
+
#
|
|
120
|
+
# @rbs () -> void
|
|
121
|
+
def materialize!
|
|
122
|
+
return if @registry
|
|
123
|
+
|
|
124
|
+
@registry = @parent.registry.map(&:dup)
|
|
125
|
+
@parent = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
# Recursively execute middleware in the chain.
|
|
109
129
|
#
|
|
110
130
|
# @param index [Integer] Current middleware index in the chain
|
|
@@ -129,8 +129,10 @@ module CMDx
|
|
|
129
129
|
# @return [Hash] The thread or fiber storage
|
|
130
130
|
#
|
|
131
131
|
# @rbs () -> Hash
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
if Fiber.respond_to?(:storage)
|
|
133
|
+
def thread_or_fiber = Fiber.storage
|
|
134
|
+
else
|
|
135
|
+
def thread_or_fiber = Thread.current
|
|
134
136
|
end
|
|
135
137
|
|
|
136
138
|
end
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
|
|
5
|
-
# Error raised when task execution exceeds the configured timeout limit.
|
|
6
|
-
#
|
|
7
|
-
# This error occurs when a task takes longer to execute than the specified
|
|
8
|
-
# time limit. Timeout errors are raised by Ruby's Timeout module and are
|
|
9
|
-
# caught by the middleware to properly fail the task with timeout information.
|
|
10
|
-
TimeoutError = Class.new(Interrupt)
|
|
11
|
-
|
|
12
4
|
module Middlewares
|
|
13
5
|
# Middleware for enforcing execution time limits on tasks.
|
|
14
6
|
#
|
|
@@ -66,12 +58,21 @@ module CMDx
|
|
|
66
58
|
else callable.respond_to?(:call) ? callable.call(task) : DEFAULT_LIMIT
|
|
67
59
|
end
|
|
68
60
|
|
|
61
|
+
limit = Float(limit)
|
|
62
|
+
return yield unless limit.positive?
|
|
63
|
+
|
|
69
64
|
::Timeout.timeout(limit, TimeoutError, "execution exceeded #{limit} seconds", &)
|
|
70
65
|
rescue TimeoutError => e
|
|
71
|
-
task.result.tap
|
|
66
|
+
task.result.tap do |r|
|
|
67
|
+
r.fail!(
|
|
68
|
+
Utils::Normalize.exception(e),
|
|
69
|
+
cause: e,
|
|
70
|
+
source: :timeout,
|
|
71
|
+
limit:
|
|
72
|
+
)
|
|
73
|
+
end
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
|
-
|
|
77
78
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Bounded thread pool that processes items concurrently.
|
|
5
|
+
#
|
|
6
|
+
# Distributes work across a fixed number of threads using a queue,
|
|
7
|
+
# collecting results in submission order.
|
|
8
|
+
class Parallelizer
|
|
9
|
+
|
|
10
|
+
# Returns the items to process.
|
|
11
|
+
#
|
|
12
|
+
# @return [Array] the items to process
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# parallelizer.items # => [1, 2, 3]
|
|
16
|
+
#
|
|
17
|
+
# @rbs @items: Array[untyped]
|
|
18
|
+
attr_reader :items
|
|
19
|
+
|
|
20
|
+
# Returns the number of threads in the pool.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer] the thread pool size
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# parallelizer.pool_size # => 4
|
|
26
|
+
#
|
|
27
|
+
# @rbs @pool_size: Integer
|
|
28
|
+
attr_reader :pool_size
|
|
29
|
+
|
|
30
|
+
# Creates a new Parallelizer instance.
|
|
31
|
+
#
|
|
32
|
+
# @param items [Array] the items to process concurrently
|
|
33
|
+
# @param pool_size [Integer] number of threads (defaults to item count)
|
|
34
|
+
#
|
|
35
|
+
# @return [Parallelizer] a new parallelizer instance
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# Parallelizer.new([1, 2, 3], pool_size: 2)
|
|
39
|
+
#
|
|
40
|
+
# @rbs (Array[untyped] items, ?pool_size: Integer) -> void
|
|
41
|
+
def initialize(items, pool_size: nil)
|
|
42
|
+
@items = items
|
|
43
|
+
@pool_size = Integer(pool_size || items.size)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Processes items concurrently and returns results in submission order.
|
|
47
|
+
#
|
|
48
|
+
# @param items [Array] the items to process concurrently
|
|
49
|
+
# @param pool_size [Integer] number of threads (defaults to item count)
|
|
50
|
+
#
|
|
51
|
+
# @yield [item] block called for each item in a worker thread
|
|
52
|
+
# @yieldparam item [Object] an item from the items array
|
|
53
|
+
# @yieldreturn [Object] the result for this item
|
|
54
|
+
#
|
|
55
|
+
# @return [Array] results in the same order as input items
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# Parallelizer.call([1, 2, 3], pool_size: 2) { |n| n * 10 }
|
|
59
|
+
# # => [10, 20, 30]
|
|
60
|
+
#
|
|
61
|
+
# @rbs [T, R] (Array[T] items, ?pool_size: Integer) { (T) -> R } -> Array[R]
|
|
62
|
+
def self.call(items, pool_size: nil, &block)
|
|
63
|
+
new(items, pool_size:).call(&block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Distributes items across the thread pool and returns results
|
|
67
|
+
# in submission order.
|
|
68
|
+
#
|
|
69
|
+
# @yield [item] block called for each item in a worker thread
|
|
70
|
+
# @yieldparam item [Object] an item from the items array
|
|
71
|
+
# @yieldreturn [Object] the result for this item
|
|
72
|
+
#
|
|
73
|
+
# @return [Array] results in the same order as input items
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# Parallelizer.new(%w[a b c]).call { |s| s.upcase }
|
|
77
|
+
# # => ["A", "B", "C"]
|
|
78
|
+
#
|
|
79
|
+
# @rbs [T, R] () { (T) -> R } -> Array[R]
|
|
80
|
+
def call(&block)
|
|
81
|
+
results = Array.new(items.size)
|
|
82
|
+
queue = Queue.new
|
|
83
|
+
|
|
84
|
+
items.each_with_index { |item, i| queue << [item, i] }
|
|
85
|
+
pool_size.times { queue << nil }
|
|
86
|
+
|
|
87
|
+
Array.new(pool_size) do
|
|
88
|
+
Thread.new do
|
|
89
|
+
while (entry = queue.pop)
|
|
90
|
+
item, index = entry
|
|
91
|
+
results[index] = block.call(item) # rubocop:disable Performance/RedundantBlockCall
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end.each(&:join)
|
|
95
|
+
|
|
96
|
+
results
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
end
|