cmdx 1.1.2 → 1.5.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/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +56 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/CHANGELOG.md +5 -133
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +91 -154
- data/lib/cmdx/validators/numeric.rb +87 -162
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -67
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -58
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
data/docs/middlewares.md
CHANGED
@@ -4,515 +4,202 @@ Middleware provides Rack-style wrappers around task execution for cross-cutting
|
|
4
4
|
|
5
5
|
## Table of Contents
|
6
6
|
|
7
|
-
- [
|
8
|
-
- [
|
9
|
-
- [
|
10
|
-
- [
|
11
|
-
|
12
|
-
- [
|
13
|
-
- [
|
14
|
-
- [
|
15
|
-
- [
|
16
|
-
- [Timeout Middleware](#timeout-middleware)
|
17
|
-
- [Correlate Middleware](#correlate-middleware)
|
18
|
-
- [Writing Custom Middleware](#writing-custom-middleware)
|
19
|
-
- [Error Handling](#error-handling)
|
20
|
-
|
21
|
-
## TLDR
|
7
|
+
- [Order](#order)
|
8
|
+
- [Declarations](#declarations)
|
9
|
+
- [Proc or Lambda](#proc-or-lambda)
|
10
|
+
- [Class or Module](#class-or-module)
|
11
|
+
- [Removals](#removals)
|
12
|
+
- [Built-in](#built-in)
|
13
|
+
- [Timeout](#timeout)
|
14
|
+
- [Correlate](#correlate)
|
15
|
+
- [Runtime](#runtime)
|
22
16
|
|
23
|
-
|
24
|
-
# Declare middleware with use method
|
25
|
-
use :middleware, AuthMiddleware, role: :admin # Class with options
|
26
|
-
use :middleware, LoggingMiddleware.new(level: :debug) # Instance
|
27
|
-
use :middleware, proc { |task, callable| ... } # Proc
|
28
|
-
|
29
|
-
# Execution order: first declared wraps all others
|
30
|
-
use :middleware, OuterMiddleware # Runs first/last
|
31
|
-
use :middleware, InnerMiddleware # Runs last/first
|
32
|
-
|
33
|
-
# Built-in middleware
|
34
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 30
|
35
|
-
use :middleware, CMDx::Middlewares::Correlate, id: "request-123"
|
36
|
-
```
|
17
|
+
## Order
|
37
18
|
|
38
|
-
|
19
|
+
Middleware executes in a nested fashion, creating an onion-like execution pattern:
|
39
20
|
|
40
21
|
> [!NOTE]
|
41
|
-
> Middleware executes in
|
42
|
-
|
43
|
-
### Class Middleware
|
44
|
-
|
45
|
-
The most common pattern - pass the middleware class with optional initialization arguments:
|
46
|
-
|
47
|
-
```ruby
|
48
|
-
class AuditMiddleware < CMDx::Middleware
|
49
|
-
def initialize(action:, resource_type:)
|
50
|
-
@action = action
|
51
|
-
@resource_type = resource_type
|
52
|
-
end
|
53
|
-
|
54
|
-
def call(task, callable)
|
55
|
-
result = callable.call(task)
|
56
|
-
|
57
|
-
if result.success?
|
58
|
-
AuditLog.create!(
|
59
|
-
action: @action,
|
60
|
-
resource_type: @resource_type,
|
61
|
-
resource_id: task.context.id,
|
62
|
-
user_id: task.context.current_user&.id
|
63
|
-
)
|
64
|
-
end
|
65
|
-
|
66
|
-
result
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class ProcessOrderTask < CMDx::Task
|
71
|
-
use :middleware, AuditMiddleware, action: 'process', resource_type: 'Order'
|
72
|
-
|
73
|
-
def call
|
74
|
-
context.order = Order.find(order_id)
|
75
|
-
context.order.process!
|
76
|
-
end
|
77
|
-
end
|
78
|
-
```
|
79
|
-
|
80
|
-
### Instance Middleware
|
81
|
-
|
82
|
-
Pre-configured middleware instances for complex initialization:
|
83
|
-
|
84
|
-
```ruby
|
85
|
-
class ProcessOrderTask < CMDx::Task
|
86
|
-
use :middleware, LoggingMiddleware.new(
|
87
|
-
level: :debug,
|
88
|
-
formatter: CustomFormatter.new,
|
89
|
-
tags: ['order', 'payment']
|
90
|
-
)
|
91
|
-
|
92
|
-
def call
|
93
|
-
context.order = Order.find(order_id)
|
94
|
-
context.order.process!
|
95
|
-
end
|
96
|
-
end
|
97
|
-
```
|
98
|
-
|
99
|
-
### Proc Middleware
|
100
|
-
|
101
|
-
Inline middleware for simple cases:
|
102
|
-
|
103
|
-
```ruby
|
104
|
-
class ProcessOrderTask < CMDx::Task
|
105
|
-
use :middleware, proc { |task, callable|
|
106
|
-
start_time = Time.now
|
107
|
-
result = callable.call(task)
|
108
|
-
duration = Time.now - start_time
|
109
|
-
|
110
|
-
Rails.logger.info "#{task.class.name} completed in #{duration.round(3)}s"
|
111
|
-
result
|
112
|
-
}
|
113
|
-
|
114
|
-
def call
|
115
|
-
# Business logic
|
116
|
-
end
|
117
|
-
end
|
118
|
-
```
|
119
|
-
|
120
|
-
## Execution Order
|
121
|
-
|
122
|
-
> [!IMPORTANT]
|
123
|
-
> Middleware executes in nested fashion - first declared wraps all others, creating an onion-like execution pattern.
|
22
|
+
> Middleware executes in the order they are registered, with the first registered middleware being the outermost wrapper.
|
124
23
|
|
125
24
|
```ruby
|
126
|
-
class
|
127
|
-
|
128
|
-
|
129
|
-
|
25
|
+
class ProcessCampaign < CMDx::Task
|
26
|
+
register :middleware, AuditMiddleware # 1st: outermost wrapper
|
27
|
+
register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
|
28
|
+
register :middleware, CacheMiddleware # 3rd: innermost wrapper
|
130
29
|
|
131
|
-
def
|
132
|
-
#
|
30
|
+
def work
|
31
|
+
# Your logic here...
|
133
32
|
end
|
134
33
|
end
|
135
34
|
|
136
35
|
# Execution flow:
|
137
|
-
# 1.
|
138
|
-
# 2.
|
139
|
-
# 3.
|
36
|
+
# 1. AuditMiddleware (before)
|
37
|
+
# 2. AuthorizationMiddleware (before)
|
38
|
+
# 3. CacheMiddleware (before)
|
140
39
|
# 4. [task execution]
|
141
|
-
# 5.
|
142
|
-
# 6.
|
143
|
-
# 7.
|
40
|
+
# 5. CacheMiddleware (after)
|
41
|
+
# 6. AuthorizationMiddleware (after)
|
42
|
+
# 7. AuditMiddleware (after)
|
144
43
|
```
|
145
44
|
|
146
|
-
##
|
147
|
-
|
148
|
-
> [!WARNING]
|
149
|
-
> Middleware can halt execution by not calling the next callable. This prevents the task and subsequent middleware from executing.
|
150
|
-
|
151
|
-
```ruby
|
152
|
-
class RateLimitMiddleware < CMDx::Middleware
|
153
|
-
def initialize(limit: 100, window: 1.hour)
|
154
|
-
@limit = limit
|
155
|
-
@window = window
|
156
|
-
end
|
157
|
-
|
158
|
-
def call(task, callable)
|
159
|
-
key = "rate_limit:#{task.context.current_user&.id}"
|
160
|
-
current_count = Rails.cache.read(key) || 0
|
161
|
-
|
162
|
-
if current_count >= @limit
|
163
|
-
task.fail!(reason: "Rate limit exceeded: #{@limit} requests per hour")
|
164
|
-
return task.result # Short-circuit - task never executes
|
165
|
-
end
|
166
|
-
|
167
|
-
Rails.cache.write(key, current_count + 1, expires_in: @window)
|
168
|
-
callable.call(task)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
class SendEmailTask < CMDx::Task
|
173
|
-
use :middleware, RateLimitMiddleware, limit: 50
|
174
|
-
|
175
|
-
def call
|
176
|
-
# Only executes if rate limit check passes
|
177
|
-
EmailService.deliver(email_params)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
```
|
181
|
-
|
182
|
-
## Inheritance
|
183
|
-
|
184
|
-
> [!TIP]
|
185
|
-
> Middleware is inherited from parent classes, making it ideal for application-wide concerns.
|
186
|
-
|
187
|
-
```ruby
|
188
|
-
class ApplicationTask < CMDx::Task
|
189
|
-
use :middleware, RequestIdMiddleware # All tasks get request tracking
|
190
|
-
use :middleware, PerformanceMiddleware # All tasks get performance monitoring
|
191
|
-
use :middleware, ErrorReportingMiddleware # All tasks get error reporting
|
192
|
-
end
|
193
|
-
|
194
|
-
class ProcessOrderTask < ApplicationTask
|
195
|
-
use :middleware, AuthenticationMiddleware # Added to inherited middleware
|
196
|
-
use :middleware, OrderValidationMiddleware # Domain-specific validation
|
197
|
-
|
198
|
-
def call
|
199
|
-
# Inherits all ApplicationTask middleware plus order-specific ones
|
200
|
-
context.order = Order.find(order_id)
|
201
|
-
context.order.process!
|
202
|
-
end
|
203
|
-
end
|
204
|
-
```
|
205
|
-
|
206
|
-
## Built-in Middleware
|
207
|
-
|
208
|
-
### Timeout Middleware
|
209
|
-
|
210
|
-
Enforces execution time limits with support for static and dynamic timeout values.
|
211
|
-
|
212
|
-
#### Basic Usage
|
213
|
-
|
214
|
-
```ruby
|
215
|
-
class ProcessLargeReportTask < CMDx::Task
|
216
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 300
|
217
|
-
|
218
|
-
def call
|
219
|
-
# Long-running report generation with 5-minute timeout
|
220
|
-
ReportGenerator.create(report_params)
|
221
|
-
end
|
222
|
-
end
|
45
|
+
## Declarations
|
223
46
|
|
224
|
-
|
225
|
-
class QuickValidationTask < CMDx::Task
|
226
|
-
use :middleware, CMDx::Middlewares::Timeout
|
47
|
+
### Proc or Lambda
|
227
48
|
|
228
|
-
|
229
|
-
# Fast validation with default 3-second timeout
|
230
|
-
ValidationService.validate(data)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
```
|
234
|
-
|
235
|
-
#### Dynamic Timeout Calculation
|
236
|
-
|
237
|
-
> [!NOTE]
|
238
|
-
> Timeout supports method names, procs, and lambdas for dynamic calculation based on task context.
|
49
|
+
Use anonymous functions for simple middleware logic:
|
239
50
|
|
240
51
|
```ruby
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
context.order.process!
|
52
|
+
class ProcessCampaign < CMDx::Task
|
53
|
+
# Proc
|
54
|
+
register :middleware, proc do |task, options, &block|
|
55
|
+
result = block.call
|
56
|
+
Analytics.track(result.status)
|
57
|
+
result
|
248
58
|
end
|
249
59
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
base_timeout += 60 if context.payment_method == "bank_transfer"
|
256
|
-
base_timeout
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
# Proc-based timeout
|
261
|
-
class ProcessWorkflowTask < CMDx::Task
|
262
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: -> {
|
263
|
-
context.workflow_size > 100 ? 120 : 60
|
60
|
+
# Lambda
|
61
|
+
register :middleware, ->(task, options, &block) {
|
62
|
+
result = block.call
|
63
|
+
Analytics.track(result.status)
|
64
|
+
result
|
264
65
|
}
|
265
|
-
|
266
|
-
def call
|
267
|
-
context.workflow_items.each { |item| process_item(item) }
|
268
|
-
end
|
269
66
|
end
|
270
67
|
```
|
271
68
|
|
272
|
-
|
69
|
+
### Class or Module
|
273
70
|
|
274
|
-
|
275
|
-
|
276
|
-
1. **Explicit timeout value** (Integer/Float, Symbol, Proc/Lambda)
|
277
|
-
2. **Default value** of 3 seconds when no timeout resolves
|
71
|
+
For complex middleware logic, use classes or modules:
|
278
72
|
|
279
73
|
```ruby
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
#
|
286
|
-
class ProcessOrderTask < CMDx::Task
|
287
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: :might_return_nil
|
288
|
-
|
289
|
-
private
|
290
|
-
def might_return_nil
|
291
|
-
nil # Uses 3-second default
|
74
|
+
class TelemetryMiddleware
|
75
|
+
def call(task, options)
|
76
|
+
result = yield
|
77
|
+
Telemetry.record(result.status)
|
78
|
+
ensure
|
79
|
+
result # Always return result
|
292
80
|
end
|
293
81
|
end
|
294
|
-
```
|
295
82
|
|
296
|
-
|
83
|
+
class ProcessCampaign < CMDx::Task
|
84
|
+
# Class or Module
|
85
|
+
register :middleware, TelemetryMiddleware
|
297
86
|
|
298
|
-
|
299
|
-
|
300
|
-
class ProcessOrderTask < CMDx::Task
|
301
|
-
use :middleware, CMDx::Middlewares::Timeout,
|
302
|
-
seconds: 60,
|
303
|
-
unless: -> { Rails.env.development? }
|
304
|
-
|
305
|
-
def call
|
306
|
-
context.order = Order.find(order_id)
|
307
|
-
context.order.process!
|
308
|
-
end
|
309
|
-
end
|
87
|
+
# Instance
|
88
|
+
register :middleware, TelemetryMiddleware.new
|
310
89
|
|
311
|
-
#
|
312
|
-
|
313
|
-
|
314
|
-
seconds: 30,
|
315
|
-
if: :timeout_enabled?
|
316
|
-
|
317
|
-
private
|
318
|
-
|
319
|
-
def timeout_enabled?
|
320
|
-
!context.background_job?
|
321
|
-
end
|
90
|
+
# With options
|
91
|
+
register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
|
92
|
+
register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
|
322
93
|
end
|
323
94
|
```
|
324
95
|
|
325
|
-
|
96
|
+
## Removals
|
326
97
|
|
327
|
-
|
328
|
-
> Manages correlation IDs for request tracing across task boundaries, enabling distributed system observability.
|
98
|
+
Class and Module based declarations can be removed at a global and task level.
|
329
99
|
|
330
|
-
|
331
|
-
|
332
|
-
use :middleware, CMDx::Middlewares::Correlate
|
100
|
+
> [!WARNING]
|
101
|
+
> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
|
333
102
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
103
|
+
```ruby
|
104
|
+
class ProcessCampaign < CMDx::Task
|
105
|
+
# Class or Module (no instances)
|
106
|
+
deregister :middleware, TelemetryMiddleware
|
338
107
|
end
|
339
108
|
```
|
340
109
|
|
341
|
-
|
110
|
+
## Built-in
|
342
111
|
|
343
|
-
|
112
|
+
### Timeout
|
344
113
|
|
345
|
-
|
346
|
-
2. **Thread-local correlation** (CMDx::Correlator.id)
|
347
|
-
3. **Existing chain ID** (inherited from parent task)
|
348
|
-
4. **Generated UUID** (when none exist)
|
114
|
+
Ensures task execution doesn't exceed a specified time limit:
|
349
115
|
|
350
116
|
```ruby
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
end
|
355
|
-
|
356
|
-
# Dynamic correlation ID
|
357
|
-
class ProcessOrderTask < CMDx::Task
|
358
|
-
use :middleware, CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}" }
|
359
|
-
end
|
360
|
-
|
361
|
-
# Method-based correlation ID
|
362
|
-
class ProcessApiRequestTask < CMDx::Task
|
363
|
-
use :middleware, CMDx::Middlewares::Correlate, id: :generate_correlation_id
|
364
|
-
|
365
|
-
private
|
366
|
-
|
367
|
-
def generate_correlation_id
|
368
|
-
"api-#{context.request_id}-#{context.user_id}"
|
369
|
-
end
|
370
|
-
end
|
371
|
-
```
|
117
|
+
class ProcessReport < CMDx::Task
|
118
|
+
# Default timeout: 3 seconds
|
119
|
+
register :middleware, CMDx::Middlewares::Timeout
|
372
120
|
|
373
|
-
|
121
|
+
# Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
|
122
|
+
register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
|
374
123
|
|
375
|
-
|
376
|
-
|
377
|
-
before_action :set_correlation_id
|
124
|
+
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
125
|
+
register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
|
378
126
|
|
379
|
-
def
|
380
|
-
|
381
|
-
|
382
|
-
if result.success?
|
383
|
-
render json: { order: result.context.order, correlation_id: result.chain.id }
|
384
|
-
else
|
385
|
-
render json: { error: result.reason }, status: 422
|
386
|
-
end
|
127
|
+
def work
|
128
|
+
# Your logic here...
|
387
129
|
end
|
388
130
|
|
389
131
|
private
|
390
132
|
|
391
|
-
def
|
392
|
-
|
393
|
-
CMDx::Correlator.id = correlation_id
|
394
|
-
response.headers['X-Correlation-ID'] = correlation_id
|
133
|
+
def max_processing_time
|
134
|
+
Rails.env.production? ? 2 : 10
|
395
135
|
end
|
396
136
|
end
|
397
137
|
|
398
|
-
|
399
|
-
|
138
|
+
# Slow task
|
139
|
+
result = ProcessReport.execute
|
400
140
|
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
end
|
407
|
-
end
|
141
|
+
result.state #=> "interrupted"
|
142
|
+
result.status #=> "failure"
|
143
|
+
result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
|
144
|
+
result.cause #=> <CMDx::TimeoutError>
|
145
|
+
result.metadata #=> { limit: 3 }
|
408
146
|
```
|
409
147
|
|
410
|
-
|
148
|
+
### Correlate
|
411
149
|
|
412
|
-
|
413
|
-
> Custom middleware must inherit from `CMDx::Middleware` and implement the `call(task, callable)` method.
|
150
|
+
Tags tasks with a global correlation ID for distributed tracing:
|
414
151
|
|
415
152
|
```ruby
|
416
|
-
class
|
417
|
-
|
418
|
-
|
419
|
-
result = callable.call(task)
|
153
|
+
class ProcessExport < CMDx::Task
|
154
|
+
# Default correlation ID generation
|
155
|
+
register :middleware, CMDx::Middlewares::Correlate
|
420
156
|
|
421
|
-
|
422
|
-
|
157
|
+
# Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
|
158
|
+
register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
|
423
159
|
|
424
|
-
|
425
|
-
|
426
|
-
end
|
427
|
-
end
|
160
|
+
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
161
|
+
register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
|
428
162
|
|
429
|
-
|
430
|
-
|
431
|
-
@ttl = ttl
|
432
|
-
@key_prefix = key_prefix
|
433
|
-
end
|
434
|
-
|
435
|
-
def call(task, callable)
|
436
|
-
cache_key = build_cache_key(task)
|
437
|
-
cached_result = Rails.cache.read(cache_key)
|
438
|
-
|
439
|
-
return cached_result if cached_result
|
440
|
-
|
441
|
-
result = callable.call(task)
|
442
|
-
|
443
|
-
if result.success?
|
444
|
-
Rails.cache.write(cache_key, result, expires_in: @ttl)
|
445
|
-
end
|
446
|
-
|
447
|
-
result
|
163
|
+
def work
|
164
|
+
# Your logic here...
|
448
165
|
end
|
449
166
|
|
450
167
|
private
|
451
168
|
|
452
|
-
def
|
453
|
-
|
454
|
-
param_hash = Digest::MD5.hexdigest(task.context.to_h.to_json)
|
455
|
-
[@key_prefix, base_key, param_hash].compact.join(':')
|
169
|
+
def correlation_enabled?
|
170
|
+
ENV["CORRELATION_ENABLED"] == "true"
|
456
171
|
end
|
457
172
|
end
|
458
|
-
```
|
459
173
|
|
460
|
-
|
174
|
+
result = ProcessExport.execute
|
175
|
+
result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
|
176
|
+
```
|
461
177
|
|
462
|
-
|
463
|
-
> Middleware errors can prevent task execution. Handle exceptions appropriately and consider their impact on the execution chain.
|
178
|
+
### Runtime
|
464
179
|
|
465
|
-
|
180
|
+
The runtime middleware tags tasks with how long it took to execute the task.
|
181
|
+
The calculation uses a monotonic clock and the time is returned in milliseconds.
|
466
182
|
|
467
183
|
```ruby
|
468
|
-
class
|
469
|
-
def call(task
|
470
|
-
|
471
|
-
raise "Configuration missing" unless configured?
|
472
|
-
|
473
|
-
callable.call(task)
|
474
|
-
rescue StandardError => e
|
475
|
-
# Handle middleware-specific errors
|
476
|
-
task.fail!(reason: "Middleware error: #{e.message}")
|
477
|
-
task.result
|
184
|
+
class PerformanceMonitoringCheck
|
185
|
+
def call(task)
|
186
|
+
task.context.tenant.monitoring_enabled?
|
478
187
|
end
|
479
188
|
end
|
480
189
|
|
481
|
-
|
482
|
-
|
483
|
-
|
190
|
+
class ProcessExport < CMDx::Task
|
191
|
+
# Default timeout is 3 seconds
|
192
|
+
register :middleware, CMDx::Middlewares::Runtime
|
484
193
|
|
485
|
-
|
486
|
-
|
487
|
-
end
|
194
|
+
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
195
|
+
register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
|
488
196
|
end
|
489
197
|
|
490
|
-
result =
|
491
|
-
result.
|
492
|
-
result.reason # → "Task timed out after 5 seconds"
|
493
|
-
```
|
494
|
-
|
495
|
-
### Middleware Error Recovery
|
496
|
-
|
497
|
-
```ruby
|
498
|
-
class ResilientMiddleware < CMDx::Middleware
|
499
|
-
def call(task, callable)
|
500
|
-
callable.call(task)
|
501
|
-
rescue ExternalServiceError => e
|
502
|
-
# Log error but allow task to complete
|
503
|
-
Rails.logger.error "External service unavailable: #{e.message}"
|
504
|
-
|
505
|
-
# Continue execution with degraded functionality
|
506
|
-
task.context.external_service_available = false
|
507
|
-
callable.call(task)
|
508
|
-
end
|
509
|
-
end
|
198
|
+
result = ProcessExport.execute
|
199
|
+
result.metadata #=> { runtime: 1247 } (ms)
|
510
200
|
```
|
511
201
|
|
512
|
-
> [!TIP]
|
513
|
-
> Design middleware to fail gracefully when possible. Consider whether middleware failure should prevent task execution or allow degraded operation.
|
514
|
-
|
515
202
|
---
|
516
203
|
|
517
204
|
- **Prev:** [Callbacks](callbacks.md)
|
518
|
-
- **Next:** [
|
205
|
+
- **Next:** [Logging](logging.md)
|