cmdx 1.0.1 → 1.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/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -1
- data/docs/basics/call.md +2 -2
- data/docs/basics/chain.md +1 -1
- data/docs/callbacks.md +3 -36
- data/docs/configuration.md +58 -12
- data/docs/interruptions/exceptions.md +1 -1
- data/docs/interruptions/faults.md +2 -2
- data/docs/logging.md +4 -4
- data/docs/middlewares.md +43 -43
- data/docs/parameters/coercions.md +49 -38
- data/docs/parameters/defaults.md +1 -1
- data/docs/parameters/validations.md +0 -39
- data/docs/testing.md +11 -12
- data/docs/workflows.md +4 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +34 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,255 +2,70 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module Middlewares
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# The Correlate middleware establishes and maintains correlation ID context throughout
|
9
|
-
# task execution, enabling seamless request tracking across task boundaries. It ensures
|
10
|
-
# that all tasks within an execution chain share the same correlation identifier for
|
11
|
-
# comprehensive traceability and debugging.
|
12
|
-
#
|
13
|
-
# ## Correlation ID Precedence
|
14
|
-
#
|
15
|
-
# The middleware determines the correlation ID using the following precedence:
|
16
|
-
# 1. **Explicit correlation ID** - Value provided during middleware initialization
|
17
|
-
# 2. **Current thread correlation** - Existing correlation from `CMDx::Correlator.id`
|
18
|
-
# 3. **Chain identifier** - The task's chain ID if no thread correlation exists
|
19
|
-
# 4. **Generated UUID** - New correlation ID if none of the above is available
|
20
|
-
#
|
21
|
-
# ## Conditional Execution
|
22
|
-
#
|
23
|
-
# The middleware supports conditional execution using `:if` and `:unless` options:
|
24
|
-
# - `:if` - Only applies correlation when the condition evaluates to true
|
25
|
-
# - `:unless` - Only applies correlation when the condition evaluates to false
|
26
|
-
# - Conditions can be Procs, method symbols, or boolean values
|
27
|
-
#
|
28
|
-
# ## Thread Safety
|
29
|
-
#
|
30
|
-
# The middleware uses `CMDx::Correlator.use` to establish a correlation context
|
31
|
-
# that is automatically restored after task execution, ensuring thread-local
|
32
|
-
# isolation and proper cleanup even in case of exceptions.
|
33
|
-
#
|
34
|
-
# ## Integration with CMDx Framework
|
35
|
-
#
|
36
|
-
# - **Automatic activation**: Can be applied globally or per-task via `use` directive
|
37
|
-
# - **Chain integration**: Works seamlessly with CMDx::Chain correlation inheritance
|
38
|
-
# - **Nested tasks**: Maintains correlation context across nested task calls
|
39
|
-
# - **Exception safety**: Restores correlation context even when tasks fail
|
40
|
-
#
|
41
|
-
# @example Basic task-specific middleware application
|
42
|
-
# class ProcessOrderTask < CMDx::Task
|
43
|
-
# use CMDx::Middlewares::Correlate
|
44
|
-
#
|
45
|
-
# def call
|
46
|
-
# # Task execution maintains correlation context
|
47
|
-
# SendEmailTask.call(context) # Inherits same correlation
|
48
|
-
# end
|
49
|
-
# end
|
50
|
-
#
|
51
|
-
# @example Middleware with explicit correlation ID
|
52
|
-
# class ProcessOrderTask < CMDx::Task
|
53
|
-
# use CMDx::Middlewares::Correlate, id: "order-processing-123"
|
54
|
-
#
|
55
|
-
# def call
|
56
|
-
# # Always uses "order-processing-123" as correlation ID
|
57
|
-
# context.correlation_used = CMDx::Correlator.id
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
62
|
-
# result.context.correlation_used # => "order-processing-123"
|
63
|
-
#
|
64
|
-
# @example Middleware with dynamic correlation ID using procs
|
65
|
-
# class ProcessOrderTask < CMDx::Task
|
66
|
-
# use CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}-#{Time.now.to_i}" }
|
67
|
-
#
|
68
|
-
# def call
|
69
|
-
# # Uses dynamically generated correlation ID
|
70
|
-
# context.correlation_used = CMDx::Correlator.id
|
71
|
-
# end
|
72
|
-
# end
|
73
|
-
#
|
74
|
-
# result = ProcessOrderTask.call(order_id: 456)
|
75
|
-
# result.context.correlation_used # => "order-456-1703123456"
|
76
|
-
#
|
77
|
-
# @example Middleware with method-based correlation ID
|
78
|
-
# class ProcessOrderTask < CMDx::Task
|
79
|
-
# use CMDx::Middlewares::Correlate, id: :generate_order_correlation
|
80
|
-
#
|
81
|
-
# def call
|
82
|
-
# # Uses correlation ID from generate_order_correlation method
|
83
|
-
# context.correlation_used = CMDx::Correlator.id
|
84
|
-
# end
|
85
|
-
#
|
86
|
-
# private
|
87
|
-
#
|
88
|
-
# def generate_order_correlation
|
89
|
-
# "order-#{order_id}-#{context.request_id}"
|
90
|
-
# end
|
91
|
-
# end
|
92
|
-
#
|
93
|
-
# @example Conditional correlation based on environment
|
94
|
-
# class ProcessOrderTask < CMDx::Task
|
95
|
-
# use CMDx::Middlewares::Correlate, unless: -> { Rails.env.test? }
|
96
|
-
#
|
97
|
-
# def call
|
98
|
-
# # Correlation only applied in non-test environments
|
99
|
-
# context.order = Order.find(order_id)
|
100
|
-
# end
|
101
|
-
# end
|
102
|
-
#
|
103
|
-
# @example Conditional correlation based on task state
|
104
|
-
# class ProcessOrderTask < CMDx::Task
|
105
|
-
# use CMDx::Middlewares::Correlate, if: :correlation_required?
|
106
|
-
#
|
107
|
-
# def call
|
108
|
-
# # Correlation applied only when correlation_required? returns true
|
109
|
-
# context.order = Order.find(order_id)
|
110
|
-
# end
|
111
|
-
#
|
112
|
-
# private
|
113
|
-
#
|
114
|
-
# def correlation_required?
|
115
|
-
# context.tracking_enabled == true
|
116
|
-
# end
|
117
|
-
# end
|
118
|
-
#
|
119
|
-
# @example Nested task correlation propagation
|
120
|
-
# class ParentTask < CMDx::Task
|
121
|
-
# use CMDx::Middlewares::Correlate
|
122
|
-
#
|
123
|
-
# def call
|
124
|
-
# # Correlation established at parent level
|
125
|
-
# ChildTask.call(context)
|
126
|
-
# end
|
127
|
-
# end
|
128
|
-
#
|
129
|
-
# class ChildTask < CMDx::Task
|
130
|
-
# use CMDx::Middlewares::Correlate
|
131
|
-
#
|
132
|
-
# def call
|
133
|
-
# # Inherits parent's correlation ID
|
134
|
-
# context.child_correlation = CMDx::Correlator.id
|
135
|
-
# end
|
136
|
-
# end
|
137
|
-
#
|
138
|
-
# @example Exception handling with correlation restoration
|
139
|
-
# class RiskyTask < CMDx::Task
|
140
|
-
# use CMDx::Middlewares::Correlate
|
141
|
-
#
|
142
|
-
# def call
|
143
|
-
# raise StandardError, "Task failed"
|
144
|
-
# end
|
145
|
-
# end
|
146
|
-
#
|
147
|
-
# CMDx::Correlator.id = "original-correlation"
|
148
|
-
#
|
149
|
-
# begin
|
150
|
-
# RiskyTask.call
|
151
|
-
# rescue StandardError
|
152
|
-
# CMDx::Correlator.id # => "original-correlation" (properly restored)
|
153
|
-
# end
|
154
|
-
#
|
155
|
-
# @see CMDx::Correlator Thread-safe correlation ID management
|
156
|
-
# @see CMDx::Chain Chain execution context with correlation inheritance
|
157
|
-
# @see CMDx::Middleware Base middleware class
|
158
|
-
# @since 1.0.0
|
5
|
+
# Middleware that manages correlation IDs for task execution tracing.
|
6
|
+
# Automatically generates or uses provided correlation IDs to track task execution
|
7
|
+
# across complex workflows, enabling better debugging and monitoring.
|
159
8
|
class Correlate < CMDx::Middleware
|
160
9
|
|
161
|
-
# @return [String, nil] The explicit correlation ID to use
|
10
|
+
# @return [String, Symbol, Proc, nil] The explicit correlation ID to use, or callable that generates one
|
11
|
+
attr_reader :id
|
12
|
+
|
162
13
|
# @return [Hash] The conditional options for correlation application
|
163
|
-
attr_reader :
|
14
|
+
attr_reader :conditional
|
164
15
|
|
165
|
-
|
166
|
-
# Initializes the Correlate middleware with optional configuration.
|
16
|
+
# Initializes the correlation middleware with optional configuration.
|
167
17
|
#
|
168
18
|
# @param options [Hash] configuration options for the middleware
|
169
|
-
# @option options [String, Symbol, Proc] :id explicit correlation ID
|
170
|
-
# @option options [
|
171
|
-
# @option options [
|
19
|
+
# @option options [String, Symbol, Proc] :id explicit correlation ID or callable to generate one
|
20
|
+
# @option options [Symbol, Proc] :if condition that must be truthy to apply correlation
|
21
|
+
# @option options [Symbol, Proc] :unless condition that must be falsy to apply correlation
|
22
|
+
#
|
23
|
+
# @return [Correlate] new instance of the middleware
|
24
|
+
#
|
25
|
+
# @example Register with a middleware instance
|
26
|
+
# use :middleware, CMDx::Middlewares::Correlate.new(id: "request-123")
|
172
27
|
#
|
173
|
-
# @example
|
174
|
-
# middleware
|
28
|
+
# @example Register with explicit ID
|
29
|
+
# use :middleware, CMDx::Middlewares::Correlate, id: "request-123"
|
175
30
|
#
|
176
|
-
# @example
|
177
|
-
# middleware
|
31
|
+
# @example Register with dynamic ID generation
|
32
|
+
# use :middleware, CMDx::Middlewares::Correlate, id: -> { SecureRandom.uuid }
|
178
33
|
#
|
179
|
-
# @example
|
180
|
-
# middleware
|
181
|
-
# middleware = CMDx::Middlewares::Correlate.new(if: :correlation_enabled?)
|
34
|
+
# @example Register with conditions
|
35
|
+
# use :middleware, CMDx::Middlewares::Correlate, if: :production?, unless: :testing?
|
182
36
|
def initialize(options = {})
|
183
37
|
@id = options[:id]
|
184
38
|
@conditional = options.slice(:if, :unless)
|
185
39
|
end
|
186
40
|
|
187
|
-
|
188
|
-
#
|
189
|
-
#
|
190
|
-
# First evaluates any conditional execution rules (`:if` or `:unless` options).
|
191
|
-
# If conditions allow execution, establishes a correlation ID using the
|
192
|
-
# precedence hierarchy and executes the task within that correlation context.
|
193
|
-
# The correlation ID is automatically restored after task completion, ensuring
|
194
|
-
# proper cleanup and thread isolation.
|
195
|
-
#
|
196
|
-
# The correlation ID determination follows this precedence:
|
197
|
-
# 1. Explicit correlation ID (provided during middleware initialization)
|
198
|
-
# - String/Symbol: Used as-is or called as method if task responds to it
|
199
|
-
# - Proc/Lambda: Executed in task context for dynamic generation
|
200
|
-
# 2. Current thread correlation (CMDx::Correlator.id)
|
201
|
-
# 3. Task's chain ID (task.chain.id)
|
202
|
-
# 4. Generated UUID (CMDx::Correlator.generate)
|
203
|
-
#
|
204
|
-
# @param task [CMDx::Task] the task instance to execute
|
205
|
-
# @param callable [#call] the callable that executes the task
|
206
|
-
# @return [CMDx::Result] the task execution result
|
207
|
-
#
|
208
|
-
# @example Basic middleware execution
|
209
|
-
# middleware = CMDx::Middlewares::Correlate.new
|
210
|
-
# task = ProcessOrderTask.new(order_id: 123)
|
211
|
-
# callable = -> { task.call }
|
212
|
-
#
|
213
|
-
# result = middleware.call(task, callable)
|
214
|
-
# # Task executed within correlation context
|
215
|
-
#
|
216
|
-
# @example Correlation ID precedence in action
|
217
|
-
# # Scenario 1: Explicit string correlation ID takes precedence
|
218
|
-
# middleware = CMDx::Middlewares::Correlate.new(id: "explicit-123")
|
219
|
-
# middleware.call(task, callable) # Uses "explicit-123"
|
220
|
-
#
|
221
|
-
# # Scenario 2: Dynamic correlation ID using proc
|
222
|
-
# middleware = CMDx::Middlewares::Correlate.new(id: -> { "dynamic-#{order_id}" })
|
223
|
-
# middleware.call(task, callable) # Uses result of proc execution
|
41
|
+
# Executes the middleware, wrapping task execution with correlation context.
|
42
|
+
# Evaluates conditions, determines correlation ID, and executes the task within
|
43
|
+
# the correlation context for tracing purposes.
|
224
44
|
#
|
225
|
-
#
|
226
|
-
#
|
227
|
-
# middleware.call(task, callable) # Uses task.correlation_method if it exists
|
45
|
+
# @param task [CMDx::Task] the task being executed
|
46
|
+
# @param callable [Proc] the callable that executes the task
|
228
47
|
#
|
229
|
-
#
|
230
|
-
# CMDx::Correlator.id = "thread-correlation"
|
231
|
-
# middleware = CMDx::Middlewares::Correlate.new
|
232
|
-
# middleware.call(task, callable) # Uses "thread-correlation"
|
48
|
+
# @return [Object] the result of the task execution
|
233
49
|
#
|
234
|
-
#
|
235
|
-
# CMDx::
|
236
|
-
#
|
50
|
+
# @example Task using correlation middleware
|
51
|
+
# class ProcessOrderTask < CMDx::Task
|
52
|
+
# use :middleware, CMDx::Middlewares::Correlate, id: "trace-123"
|
237
53
|
#
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
#
|
54
|
+
# def call
|
55
|
+
# # Task execution is automatically wrapped with correlation
|
56
|
+
# end
|
57
|
+
# end
|
242
58
|
#
|
243
|
-
# @example
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
247
|
-
# # Correlation applied only in production environment
|
59
|
+
# @example Global configuration with conditional tracing
|
60
|
+
# CMDx.configure do |config|
|
61
|
+
# config.middlewares.register CMDx::Middlewares::Correlate, if: :should_trace?
|
62
|
+
# end
|
248
63
|
def call(task, callable)
|
249
64
|
# Check if correlation should be applied based on conditions
|
250
|
-
return callable.call(task) unless task.
|
65
|
+
return callable.call(task) unless task.cmdx_eval(conditional)
|
251
66
|
|
252
67
|
# Get correlation ID using yield for dynamic generation
|
253
|
-
correlation_id = task.
|
68
|
+
correlation_id = task.cmdx_yield(id) ||
|
254
69
|
CMDx::Correlator.id ||
|
255
70
|
task.chain.id ||
|
256
71
|
CMDx::Correlator.generate
|
@@ -2,220 +2,81 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
-
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# This middleware wraps task execution with timeout protection, automatically
|
9
|
-
# failing tasks that exceed their configured timeout duration. The timeout
|
10
|
-
# value can be static, dynamic, or method-based. If no timeout is specified,
|
11
|
-
# it defaults to 3 seconds. Optionally supports conditional timeout application
|
12
|
-
# based on task or context state.
|
13
|
-
#
|
14
|
-
# ## Timeout Value Types
|
15
|
-
#
|
16
|
-
# The middleware supports multiple ways to specify timeout values:
|
17
|
-
# - **Static values** (Integer/Float): Fixed timeout duration
|
18
|
-
# - **Method symbols**: Calls the specified method on the task for dynamic calculation
|
19
|
-
# - **Procs/Lambdas**: Executed in task context for runtime timeout determination
|
20
|
-
#
|
21
|
-
# ## Conditional Execution
|
22
|
-
#
|
23
|
-
# The middleware supports conditional timeout application using `:if` and `:unless` options:
|
24
|
-
# - `:if` - Only applies timeout when the condition evaluates to true
|
25
|
-
# - `:unless` - Only applies timeout when the condition evaluates to false
|
26
|
-
# - Conditions can be Procs, method symbols, or boolean values
|
27
|
-
#
|
28
|
-
# @example Static timeout configuration
|
29
|
-
# class ProcessOrderTask < CMDx::Task
|
30
|
-
# use CMDx::Middlewares::Timeout, seconds: 30 # 30 seconds
|
31
|
-
#
|
32
|
-
# def call
|
33
|
-
# # Task logic that might take too long
|
34
|
-
# end
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# @example Dynamic timeout using proc
|
38
|
-
# class ProcessOrderTask < CMDx::Task
|
39
|
-
# use CMDx::Middlewares::Timeout, seconds: -> { complex_order? ? 60 : 30 }
|
40
|
-
#
|
41
|
-
# def call
|
42
|
-
# # Task logic with dynamic timeout based on order complexity
|
43
|
-
# end
|
44
|
-
#
|
45
|
-
# private
|
46
|
-
#
|
47
|
-
# def complex_order?
|
48
|
-
# context.order_items.count > 10
|
49
|
-
# end
|
50
|
-
# end
|
51
|
-
#
|
52
|
-
# @example Method-based timeout
|
53
|
-
# class ProcessOrderTask < CMDx::Task
|
54
|
-
# use CMDx::Middlewares::Timeout, seconds: :calculate_timeout
|
55
|
-
#
|
56
|
-
# def call
|
57
|
-
# # Task logic with method-calculated timeout
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
# private
|
61
|
-
#
|
62
|
-
# def calculate_timeout
|
63
|
-
# base_timeout = 30
|
64
|
-
# base_timeout += (context.order_items.count * 2)
|
65
|
-
# base_timeout
|
66
|
-
# end
|
67
|
-
# end
|
68
|
-
#
|
69
|
-
# @example Using default timeout (3 seconds)
|
70
|
-
# class QuickTask < CMDx::Task
|
71
|
-
# use CMDx::Middlewares::Timeout # 3 seconds default
|
72
|
-
#
|
73
|
-
# def call
|
74
|
-
# # Task logic with default timeout
|
75
|
-
# end
|
76
|
-
# end
|
77
|
-
#
|
78
|
-
# @example Conditional timeout based on task context
|
79
|
-
# class ProcessOrderTask < CMDx::Task
|
80
|
-
# use CMDx::Middlewares::Timeout,
|
81
|
-
# seconds: 30,
|
82
|
-
# if: proc { context.enable_timeout? }
|
83
|
-
#
|
84
|
-
# def call
|
85
|
-
# # Task logic with conditional timeout
|
86
|
-
# end
|
87
|
-
# end
|
88
|
-
#
|
89
|
-
# @example Conditional timeout with method reference
|
90
|
-
# class ProcessOrderTask < CMDx::Task
|
91
|
-
# use CMDx::Middlewares::Timeout,
|
92
|
-
# seconds: 60,
|
93
|
-
# unless: :skip_timeout?
|
94
|
-
#
|
95
|
-
# def call
|
96
|
-
# # Task logic
|
97
|
-
# end
|
98
|
-
#
|
99
|
-
# private
|
100
|
-
#
|
101
|
-
# def skip_timeout?
|
102
|
-
# Rails.env.development?
|
103
|
-
# end
|
104
|
-
# end
|
105
|
-
#
|
106
|
-
# @example Global timeout middleware
|
107
|
-
# class ApplicationTask < CMDx::Task
|
108
|
-
# use CMDx::Middlewares::Timeout, seconds: 60 # Default 60 seconds
|
109
|
-
# end
|
110
|
-
#
|
111
|
-
# @see CMDx::Middleware Base middleware class
|
112
|
-
# @see CMDx::Task Task settings configuration
|
113
|
-
# @see CMDx::Workflow Workflow execution context
|
114
|
-
|
115
|
-
##
|
116
|
-
# Custom timeout error class that inherits from Interrupt.
|
117
|
-
#
|
118
|
-
# This error is raised when task execution exceeds the configured timeout
|
119
|
-
# duration. It provides a clean way to distinguish timeout errors from
|
120
|
-
# other types of interruptions or exceptions.
|
121
|
-
#
|
122
|
-
# @example Catching timeout errors
|
123
|
-
# begin
|
124
|
-
# task.call
|
125
|
-
# rescue CMDx::TimeoutError => e
|
126
|
-
# puts "Task timed out: #{e.message}"
|
127
|
-
# end
|
128
|
-
#
|
129
|
-
# @see CMDx::Middlewares::Timeout The middleware that raises this error
|
5
|
+
# Custom exception raised when task execution exceeds the configured timeout limit.
|
6
|
+
# Inherits from Interrupt to provide consistent error handling for timeout scenarios
|
7
|
+
# and allow proper interruption of long-running tasks.
|
130
8
|
TimeoutError = Class.new(Interrupt)
|
131
9
|
|
132
10
|
module Middlewares
|
11
|
+
# Middleware that provides execution timeout protection for tasks.
|
12
|
+
# Automatically interrupts task execution if it exceeds the specified time limit,
|
13
|
+
# preventing runaway processes and ensuring system responsiveness.
|
133
14
|
class Timeout < CMDx::Middleware
|
134
15
|
|
135
16
|
# @return [Integer, Float, Symbol, Proc] The timeout value in seconds
|
17
|
+
attr_reader :seconds
|
18
|
+
|
136
19
|
# @return [Hash] The conditional options for timeout application
|
137
|
-
attr_reader :
|
20
|
+
attr_reader :conditional
|
138
21
|
|
139
|
-
|
140
|
-
# Initializes the timeout middleware.
|
22
|
+
# Initializes the timeout middleware with optional configuration.
|
141
23
|
#
|
142
|
-
# @param options [Hash]
|
143
|
-
# @option options [Integer, Float, Symbol, Proc] :seconds
|
144
|
-
#
|
145
|
-
#
|
146
|
-
# - Proc/Lambda: Executed in task context for dynamic timeout calculation
|
147
|
-
# Defaults to 3 seconds if not provided.
|
148
|
-
# @option options [Symbol, Proc] :if Condition that must be truthy for timeout to be applied
|
149
|
-
# @option options [Symbol, Proc] :unless Condition that must be falsy for timeout to be applied
|
24
|
+
# @param options [Hash] configuration options for the middleware
|
25
|
+
# @option options [Integer, Float, Symbol, Proc] :seconds timeout duration in seconds (default: 3)
|
26
|
+
# @option options [Symbol, Proc] :if condition that must be truthy to apply timeout
|
27
|
+
# @option options [Symbol, Proc] :unless condition that must be falsy to apply timeout
|
150
28
|
#
|
151
|
-
# @
|
152
|
-
# CMDx::Middlewares::Timeout.new(seconds: 30)
|
29
|
+
# @return [Timeout] new instance of the middleware
|
153
30
|
#
|
154
|
-
# @example
|
155
|
-
# CMDx::Middlewares::Timeout.new(seconds:
|
31
|
+
# @example Register with a middleware instance
|
32
|
+
# use :middleware, CMDx::Middlewares::Timeout.new(seconds: 30)
|
156
33
|
#
|
157
|
-
# @example
|
158
|
-
# CMDx::Middlewares::Timeout
|
34
|
+
# @example Register with fixed timeout
|
35
|
+
# use :middleware, CMDx::Middlewares::Timeout, seconds: 30
|
159
36
|
#
|
160
|
-
# @example
|
161
|
-
# CMDx::Middlewares::Timeout.
|
37
|
+
# @example Register with dynamic timeout
|
38
|
+
# use :middleware, CMDx::Middlewares::Timeout, seconds: -> { Rails.env.test? ? 1 : 10 }
|
162
39
|
#
|
163
|
-
# @example
|
164
|
-
# CMDx::Middlewares::Timeout
|
165
|
-
# CMDx::Middlewares::Timeout.new(seconds: 60, unless: proc { Rails.env.test? })
|
40
|
+
# @example Register with conditions
|
41
|
+
# use :middleware, CMDx::Middlewares::Timeout, seconds: 5, if: :long_running?, unless: :skip_timeout?
|
166
42
|
def initialize(options = {})
|
167
43
|
@seconds = options[:seconds] || 3
|
168
44
|
@conditional = options.slice(:if, :unless)
|
169
45
|
end
|
170
46
|
|
171
|
-
|
172
|
-
#
|
173
|
-
#
|
174
|
-
# Evaluates the conditional options to determine if timeout should be applied.
|
175
|
-
# If conditions are met, resolves the timeout value using and wraps the task
|
176
|
-
# execution with a timeout mechanism that will interrupt execution if it exceeds
|
177
|
-
# the configured time limit. If conditions are not met, executes the task
|
178
|
-
# without timeout protection.
|
47
|
+
# Executes the middleware, wrapping task execution with timeout protection.
|
48
|
+
# Evaluates conditions, determines timeout duration, and executes the task within
|
49
|
+
# the timeout boundary to prevent runaway execution.
|
179
50
|
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
# - Integer/Float: Used as-is for static timeout
|
183
|
-
# - Symbol: Called as method on task if it exists, otherwise used as numeric value
|
184
|
-
# - Proc/Lambda: Executed in task context for dynamic timeout calculation
|
185
|
-
# 2. Default value of 3 seconds if no timeout is specified
|
51
|
+
# @param task [CMDx::Task] the task being executed
|
52
|
+
# @param callable [Proc] the callable that executes the task
|
186
53
|
#
|
187
|
-
# @
|
188
|
-
# @param callable [#call] The next middleware or task execution callable
|
189
|
-
# @return [CMDx::Result] The task execution result
|
190
|
-
# @raise [TimeoutError] If execution exceeds the configured timeout and conditions are met
|
54
|
+
# @return [Object] the result of the task execution
|
191
55
|
#
|
192
|
-
# @
|
193
|
-
# # Task completes in 5 seconds, timeout is 30 seconds, condition is true
|
194
|
-
# result = task.call # => success
|
56
|
+
# @raise [TimeoutError] when task execution exceeds the timeout limit
|
195
57
|
#
|
196
|
-
# @example
|
197
|
-
#
|
198
|
-
#
|
58
|
+
# @example Task using timeout middleware
|
59
|
+
# class ProcessFileTask < CMDx::Task
|
60
|
+
# use :middleware, CMDx::Middlewares::Timeout, seconds: 10
|
199
61
|
#
|
200
|
-
#
|
201
|
-
#
|
202
|
-
#
|
203
|
-
#
|
62
|
+
# def call
|
63
|
+
# # Task execution is automatically wrapped with timeout protection
|
64
|
+
# end
|
65
|
+
# end
|
204
66
|
#
|
205
|
-
# @example
|
206
|
-
#
|
207
|
-
#
|
208
|
-
#
|
209
|
-
#
|
210
|
-
# @example Condition not met
|
211
|
-
# # Task takes 60 seconds, timeout is 30 seconds, but condition is false
|
212
|
-
# result = task.call # => success (no timeout applied)
|
67
|
+
# @example Global configuration with conditional timeout
|
68
|
+
# CMDx.configure do |config|
|
69
|
+
# config.middlewares.register CMDx::Middlewares::Timeout, seconds: 30, if: :large_dataset?
|
70
|
+
# end
|
213
71
|
def call(task, callable)
|
214
72
|
# Check if timeout should be applied based on conditions
|
215
|
-
return callable.call(task) unless task.
|
73
|
+
return callable.call(task) unless task.cmdx_eval(conditional)
|
216
74
|
|
217
75
|
# Get seconds using yield for dynamic generation
|
218
|
-
limit = task.
|
76
|
+
limit = task.cmdx_yield(seconds) || 3
|
77
|
+
|
78
|
+
# Ensure limit is numeric, fallback to default if not
|
79
|
+
limit = 3 unless limit.is_a?(Numeric)
|
219
80
|
|
220
81
|
# Apply timeout protection
|
221
82
|
::Timeout.timeout(limit, TimeoutError, "execution exceeded #{limit} seconds") do
|