cmdx 1.1.0 → 1.1.1
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/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +390 -94
- data/docs/configuration.md +181 -65
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +232 -281
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
data/docs/middlewares.md
CHANGED
@@ -16,32 +16,29 @@ Middleware provides Rack-style wrappers around task execution for cross-cutting
|
|
16
16
|
- [Timeout Middleware](#timeout-middleware)
|
17
17
|
- [Correlate Middleware](#correlate-middleware)
|
18
18
|
- [Writing Custom Middleware](#writing-custom-middleware)
|
19
|
+
- [Error Handling](#error-handling)
|
19
20
|
|
20
21
|
## TLDR
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
```ruby
|
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
|
+
```
|
28
37
|
|
29
38
|
## Using Middleware
|
30
39
|
|
31
|
-
|
32
|
-
|
33
|
-
```ruby
|
34
|
-
class ProcessOrderTask < CMDx::Task
|
35
|
-
use :middleware, AuthenticationMiddleware
|
36
|
-
use :middleware, LoggingMiddleware, level: :info
|
37
|
-
use :middleware, CachingMiddleware, ttl: 300
|
38
|
-
|
39
|
-
def call
|
40
|
-
context.order = Order.find(order_id)
|
41
|
-
context.order.process!
|
42
|
-
end
|
43
|
-
end
|
44
|
-
```
|
40
|
+
> [!NOTE]
|
41
|
+
> Middleware executes in nested fashion around task execution. Use the `use` method to declare middleware in your task classes.
|
45
42
|
|
46
43
|
### Class Middleware
|
47
44
|
|
@@ -62,7 +59,7 @@ class AuditMiddleware < CMDx::Middleware
|
|
62
59
|
action: @action,
|
63
60
|
resource_type: @resource_type,
|
64
61
|
resource_id: task.context.id,
|
65
|
-
user_id: task.context.current_user
|
62
|
+
user_id: task.context.current_user&.id
|
66
63
|
)
|
67
64
|
end
|
68
65
|
|
@@ -88,12 +85,13 @@ Pre-configured middleware instances for complex initialization:
|
|
88
85
|
class ProcessOrderTask < CMDx::Task
|
89
86
|
use :middleware, LoggingMiddleware.new(
|
90
87
|
level: :debug,
|
91
|
-
formatter:
|
88
|
+
formatter: CustomFormatter.new,
|
92
89
|
tags: ['order', 'payment']
|
93
90
|
)
|
94
91
|
|
95
92
|
def call
|
96
|
-
|
93
|
+
context.order = Order.find(order_id)
|
94
|
+
context.order.process!
|
97
95
|
end
|
98
96
|
end
|
99
97
|
```
|
@@ -109,7 +107,7 @@ class ProcessOrderTask < CMDx::Task
|
|
109
107
|
result = callable.call(task)
|
110
108
|
duration = Time.now - start_time
|
111
109
|
|
112
|
-
Rails.logger.info "#{task.class.name} completed in #{duration}s"
|
110
|
+
Rails.logger.info "#{task.class.name} completed in #{duration.round(3)}s"
|
113
111
|
result
|
114
112
|
}
|
115
113
|
|
@@ -121,35 +119,34 @@ end
|
|
121
119
|
|
122
120
|
## Execution Order
|
123
121
|
|
124
|
-
|
122
|
+
> [!IMPORTANT]
|
123
|
+
> Middleware executes in nested fashion - first declared wraps all others, creating an onion-like execution pattern.
|
125
124
|
|
126
125
|
```ruby
|
127
126
|
class ProcessOrderTask < CMDx::Task
|
128
|
-
use :middleware, TimingMiddleware # 1st: outermost
|
129
|
-
use :middleware, AuthenticationMiddleware # 2nd: middle
|
130
|
-
use :middleware, ValidationMiddleware # 3rd: innermost
|
127
|
+
use :middleware, TimingMiddleware # 1st: outermost wrapper
|
128
|
+
use :middleware, AuthenticationMiddleware # 2nd: middle wrapper
|
129
|
+
use :middleware, ValidationMiddleware # 3rd: innermost wrapper
|
131
130
|
|
132
131
|
def call
|
133
|
-
# Core logic executes
|
132
|
+
# Core logic executes here
|
134
133
|
end
|
135
134
|
end
|
136
135
|
|
137
136
|
# Execution flow:
|
138
|
-
# 1. TimingMiddleware before
|
139
|
-
# 2. AuthenticationMiddleware before
|
140
|
-
# 3. ValidationMiddleware before
|
137
|
+
# 1. TimingMiddleware (before)
|
138
|
+
# 2. AuthenticationMiddleware (before)
|
139
|
+
# 3. ValidationMiddleware (before)
|
141
140
|
# 4. [task execution]
|
142
|
-
# 5. ValidationMiddleware after
|
143
|
-
# 6. AuthenticationMiddleware after
|
144
|
-
# 7. TimingMiddleware after
|
141
|
+
# 5. ValidationMiddleware (after)
|
142
|
+
# 6. AuthenticationMiddleware (after)
|
143
|
+
# 7. TimingMiddleware (after)
|
145
144
|
```
|
146
145
|
|
147
|
-
> [!IMPORTANT]
|
148
|
-
> Middleware executes in declaration order for setup and reverse order for cleanup, creating proper nesting.
|
149
|
-
|
150
146
|
## Short-circuiting
|
151
147
|
|
152
|
-
|
148
|
+
> [!WARNING]
|
149
|
+
> Middleware can halt execution by not calling the next callable. This prevents the task and subsequent middleware from executing.
|
153
150
|
|
154
151
|
```ruby
|
155
152
|
class RateLimitMiddleware < CMDx::Middleware
|
@@ -159,12 +156,12 @@ class RateLimitMiddleware < CMDx::Middleware
|
|
159
156
|
end
|
160
157
|
|
161
158
|
def call(task, callable)
|
162
|
-
key = "rate_limit:#{task.context.current_user
|
159
|
+
key = "rate_limit:#{task.context.current_user&.id}"
|
163
160
|
current_count = Rails.cache.read(key) || 0
|
164
161
|
|
165
162
|
if current_count >= @limit
|
166
163
|
task.fail!(reason: "Rate limit exceeded: #{@limit} requests per hour")
|
167
|
-
return task.result
|
164
|
+
return task.result # Short-circuit - task never executes
|
168
165
|
end
|
169
166
|
|
170
167
|
Rails.cache.write(key, current_count + 1, expires_in: @window)
|
@@ -173,22 +170,19 @@ class RateLimitMiddleware < CMDx::Middleware
|
|
173
170
|
end
|
174
171
|
|
175
172
|
class SendEmailTask < CMDx::Task
|
176
|
-
use :middleware, RateLimitMiddleware, limit: 50
|
173
|
+
use :middleware, RateLimitMiddleware, limit: 50
|
177
174
|
|
178
175
|
def call
|
179
176
|
# Only executes if rate limit check passes
|
180
|
-
EmailService.deliver(
|
181
|
-
to: email_address,
|
182
|
-
subject: subject,
|
183
|
-
body: message_body
|
184
|
-
)
|
177
|
+
EmailService.deliver(email_params)
|
185
178
|
end
|
186
179
|
end
|
187
180
|
```
|
188
181
|
|
189
182
|
## Inheritance
|
190
183
|
|
191
|
-
|
184
|
+
> [!TIP]
|
185
|
+
> Middleware is inherited from parent classes, making it ideal for application-wide concerns.
|
192
186
|
|
193
187
|
```ruby
|
194
188
|
class ApplicationTask < CMDx::Task
|
@@ -198,18 +192,17 @@ class ApplicationTask < CMDx::Task
|
|
198
192
|
end
|
199
193
|
|
200
194
|
class ProcessOrderTask < ApplicationTask
|
201
|
-
use :middleware, AuthenticationMiddleware #
|
195
|
+
use :middleware, AuthenticationMiddleware # Added to inherited middleware
|
202
196
|
use :middleware, OrderValidationMiddleware # Domain-specific validation
|
203
197
|
|
204
198
|
def call
|
205
199
|
# Inherits all ApplicationTask middleware plus order-specific ones
|
200
|
+
context.order = Order.find(order_id)
|
201
|
+
context.order.process!
|
206
202
|
end
|
207
203
|
end
|
208
204
|
```
|
209
205
|
|
210
|
-
> [!TIP]
|
211
|
-
> Middleware is inherited by subclasses, making it ideal for setting up global concerns across all tasks in your application.
|
212
|
-
|
213
206
|
## Built-in Middleware
|
214
207
|
|
215
208
|
### Timeout Middleware
|
@@ -220,34 +213,36 @@ Enforces execution time limits with support for static and dynamic timeout value
|
|
220
213
|
|
221
214
|
```ruby
|
222
215
|
class ProcessLargeReportTask < CMDx::Task
|
223
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 300
|
216
|
+
use :middleware, CMDx::Middlewares::Timeout, seconds: 300
|
224
217
|
|
225
218
|
def call
|
226
|
-
# Long-running report generation
|
219
|
+
# Long-running report generation with 5-minute timeout
|
220
|
+
ReportGenerator.create(report_params)
|
227
221
|
end
|
228
222
|
end
|
229
223
|
|
230
|
-
# Default timeout (3 seconds
|
224
|
+
# Default timeout (3 seconds)
|
231
225
|
class QuickValidationTask < CMDx::Task
|
232
|
-
use :middleware, CMDx::Middlewares::Timeout
|
226
|
+
use :middleware, CMDx::Middlewares::Timeout
|
233
227
|
|
234
228
|
def call
|
235
|
-
# Fast validation
|
229
|
+
# Fast validation with default 3-second timeout
|
230
|
+
ValidationService.validate(data)
|
236
231
|
end
|
237
232
|
end
|
238
233
|
```
|
239
234
|
|
240
|
-
#### Dynamic Timeout
|
235
|
+
#### Dynamic Timeout Calculation
|
241
236
|
|
242
|
-
|
237
|
+
> [!NOTE]
|
238
|
+
> Timeout supports method names, procs, and lambdas for dynamic calculation based on task context.
|
243
239
|
|
244
240
|
```ruby
|
245
|
-
# Method-based timeout
|
241
|
+
# Method-based timeout
|
246
242
|
class ProcessOrderTask < CMDx::Task
|
247
243
|
use :middleware, CMDx::Middlewares::Timeout, seconds: :calculate_timeout
|
248
244
|
|
249
245
|
def call
|
250
|
-
# Task execution with dynamic timeout
|
251
246
|
context.order = Order.find(order_id)
|
252
247
|
context.order.process!
|
253
248
|
end
|
@@ -255,193 +250,89 @@ class ProcessOrderTask < CMDx::Task
|
|
255
250
|
private
|
256
251
|
|
257
252
|
def calculate_timeout
|
258
|
-
# Dynamic timeout based on order complexity
|
259
253
|
base_timeout = 30
|
260
|
-
base_timeout += (context.order_items.count * 2)
|
261
|
-
base_timeout += 60 if context.payment_method == "bank_transfer"
|
254
|
+
base_timeout += (context.order_items.count * 2) # 2 seconds per item
|
255
|
+
base_timeout += 60 if context.payment_method == "bank_transfer"
|
262
256
|
base_timeout
|
263
257
|
end
|
264
258
|
end
|
265
259
|
|
266
|
-
# Proc-based timeout
|
260
|
+
# Proc-based timeout
|
267
261
|
class ProcessWorkflowTask < CMDx::Task
|
268
262
|
use :middleware, CMDx::Middlewares::Timeout, seconds: -> {
|
269
263
|
context.workflow_size > 100 ? 120 : 60
|
270
264
|
}
|
271
265
|
|
272
266
|
def call
|
273
|
-
# Processes workflow with timeout based on size
|
274
267
|
context.workflow_items.each { |item| process_item(item) }
|
275
268
|
end
|
276
269
|
end
|
277
|
-
|
278
|
-
# Context-aware timeout calculation
|
279
|
-
class GenerateReportTask < CMDx::Task
|
280
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: :report_timeout
|
281
|
-
|
282
|
-
def call
|
283
|
-
context.report = ReportGenerator.create(report_params)
|
284
|
-
end
|
285
|
-
|
286
|
-
private
|
287
|
-
|
288
|
-
def report_timeout
|
289
|
-
case context.report_type
|
290
|
-
when "summary" then 30
|
291
|
-
when "detailed" then 120
|
292
|
-
when "comprehensive" then 300
|
293
|
-
else 60
|
294
|
-
end
|
295
|
-
end
|
296
|
-
end
|
297
270
|
```
|
298
271
|
|
299
272
|
#### Timeout Precedence
|
300
273
|
|
301
|
-
The middleware
|
274
|
+
The middleware determines timeout values using this precedence:
|
302
275
|
|
303
|
-
1. **Explicit timeout value** (
|
304
|
-
|
305
|
-
- Symbol: Called as method on task if it exists
|
306
|
-
- Proc/Lambda: Executed in task context for dynamic calculation
|
307
|
-
2. **Default value** of 3 seconds if no timeout is specified or resolved value is nil
|
276
|
+
1. **Explicit timeout value** (Integer/Float, Symbol, Proc/Lambda)
|
277
|
+
2. **Default value** of 3 seconds when no timeout resolves
|
308
278
|
|
309
279
|
```ruby
|
310
|
-
# Static timeout -
|
280
|
+
# Static timeout - always 45 seconds
|
311
281
|
class ProcessOrderTask < CMDx::Task
|
312
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 45
|
282
|
+
use :middleware, CMDx::Middlewares::Timeout, seconds: 45
|
313
283
|
end
|
314
284
|
|
315
|
-
# Method
|
316
|
-
class ProcessOrderTask < CMDx::Task
|
317
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: :dynamic_timeout
|
318
|
-
|
319
|
-
private
|
320
|
-
def dynamic_timeout
|
321
|
-
context.priority == "high" ? 120 : 60
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
# Default fallback when method returns nil
|
285
|
+
# Method returns nil - falls back to 3 seconds
|
326
286
|
class ProcessOrderTask < CMDx::Task
|
327
287
|
use :middleware, CMDx::Middlewares::Timeout, seconds: :might_return_nil
|
328
288
|
|
329
289
|
private
|
330
290
|
def might_return_nil
|
331
|
-
nil
|
291
|
+
nil # Uses 3-second default
|
332
292
|
end
|
333
293
|
end
|
334
294
|
```
|
335
295
|
|
336
296
|
#### Conditional Timeout
|
337
297
|
|
338
|
-
Apply timeout middleware conditionally based on environment or task state:
|
339
|
-
|
340
298
|
```ruby
|
341
|
-
# Environment-based
|
299
|
+
# Environment-based timeout
|
342
300
|
class ProcessOrderTask < CMDx::Task
|
343
301
|
use :middleware, CMDx::Middlewares::Timeout,
|
344
302
|
seconds: 60,
|
345
303
|
unless: -> { Rails.env.development? }
|
346
304
|
|
347
305
|
def call
|
348
|
-
# No timeout in development, 60 seconds in other environments
|
349
306
|
context.order = Order.find(order_id)
|
350
307
|
context.order.process!
|
351
308
|
end
|
352
309
|
end
|
353
310
|
|
354
|
-
# Context-based
|
311
|
+
# Context-based timeout
|
355
312
|
class SendEmailTask < CMDx::Task
|
356
313
|
use :middleware, CMDx::Middlewares::Timeout,
|
357
314
|
seconds: 30,
|
358
315
|
if: :timeout_enabled?
|
359
316
|
|
360
|
-
def call
|
361
|
-
EmailService.deliver(email_params)
|
362
|
-
end
|
363
|
-
|
364
317
|
private
|
365
318
|
|
366
319
|
def timeout_enabled?
|
367
320
|
!context.background_job?
|
368
321
|
end
|
369
322
|
end
|
370
|
-
|
371
|
-
# Combined dynamic timeout with conditions
|
372
|
-
class ProcessComplexOrderTask < CMDx::Task
|
373
|
-
use :middleware, CMDx::Middlewares::Timeout,
|
374
|
-
seconds: :calculate_timeout,
|
375
|
-
unless: :skip_timeout?
|
376
|
-
|
377
|
-
def call
|
378
|
-
# Complex order processing
|
379
|
-
ValidateOrderTask.call(context)
|
380
|
-
ProcessPaymentTask.call(context)
|
381
|
-
UpdateInventoryTask.call(context)
|
382
|
-
end
|
383
|
-
|
384
|
-
private
|
385
|
-
|
386
|
-
def calculate_timeout
|
387
|
-
context.order_complexity == "high" ? 180 : 90
|
388
|
-
end
|
389
|
-
|
390
|
-
def skip_timeout?
|
391
|
-
Rails.env.test? || context.disable_timeouts?
|
392
|
-
end
|
393
|
-
end
|
394
|
-
```
|
395
|
-
|
396
|
-
#### Global Timeout Configuration
|
397
|
-
|
398
|
-
Apply timeout middleware globally with inheritance:
|
399
|
-
|
400
|
-
```ruby
|
401
|
-
class ApplicationTask < CMDx::Task
|
402
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 60 # Default 60 seconds for all tasks
|
403
|
-
end
|
404
|
-
|
405
|
-
class QuickTask < ApplicationTask
|
406
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: 15 # Override with 15 seconds
|
407
|
-
|
408
|
-
def call
|
409
|
-
# Fast operation with shorter timeout
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
class LongRunningTask < ApplicationTask
|
414
|
-
use :middleware, CMDx::Middlewares::Timeout, seconds: :dynamic_timeout
|
415
|
-
|
416
|
-
def call
|
417
|
-
# Long operation with dynamic timeout
|
418
|
-
end
|
419
|
-
|
420
|
-
private
|
421
|
-
|
422
|
-
def dynamic_timeout
|
423
|
-
context.data_size > 1000 ? 300 : 120
|
424
|
-
end
|
425
|
-
end
|
426
323
|
```
|
427
324
|
|
428
|
-
> [!WARNING]
|
429
|
-
> Tasks that exceed their timeout will be interrupted with a `CMDx::TimeoutError` and automatically marked as failed.
|
430
|
-
|
431
|
-
> [!TIP]
|
432
|
-
> Use dynamic timeout calculation to adjust execution limits based on actual task complexity, data size, or business requirements. This provides better resource utilization while maintaining appropriate safety limits.
|
433
|
-
|
434
325
|
### Correlate Middleware
|
435
326
|
|
436
|
-
|
327
|
+
> [!NOTE]
|
328
|
+
> Manages correlation IDs for request tracing across task boundaries, enabling distributed system observability.
|
437
329
|
|
438
330
|
```ruby
|
439
331
|
class ProcessApiRequestTask < CMDx::Task
|
440
332
|
use :middleware, CMDx::Middlewares::Correlate
|
441
333
|
|
442
334
|
def call
|
443
|
-
# Correlation ID
|
444
|
-
# Chain ID reflects the established correlation context
|
335
|
+
# Correlation ID automatically managed and propagated
|
445
336
|
context.api_response = ExternalService.call(request_data)
|
446
337
|
end
|
447
338
|
end
|
@@ -449,188 +340,45 @@ end
|
|
449
340
|
|
450
341
|
#### Correlation Precedence
|
451
342
|
|
452
|
-
The middleware
|
453
|
-
|
454
|
-
```ruby
|
455
|
-
# 1. Explicit correlation ID takes highest precedence
|
456
|
-
|
457
|
-
# 1a. Static string ID
|
458
|
-
class ProcessOrderTask < CMDx::Task
|
459
|
-
use :middleware, CMDx::Middlewares::Correlate, id: "fixed-correlation-123"
|
460
|
-
end
|
461
|
-
ProcessOrderTask.call # Always uses "fixed-correlation-123"
|
343
|
+
The middleware determines correlation IDs using this hierarchy:
|
462
344
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
ProcessOrderTask.call(order_id: 456) # Uses "order-456-847" (random number varies)
|
468
|
-
|
469
|
-
# 1c. Method-based ID
|
470
|
-
class ProcessOrderTask < CMDx::Task
|
471
|
-
use :middleware, CMDx::Middlewares::Correlate, id: :correlation_method
|
472
|
-
|
473
|
-
private
|
474
|
-
|
475
|
-
def correlation_method
|
476
|
-
"custom-#{order_id}"
|
477
|
-
end
|
478
|
-
end
|
479
|
-
ProcessOrderTask.call(order_id: 789) # Uses "custom-789"
|
480
|
-
|
481
|
-
# 2. Thread-local correlation when no explicit ID
|
482
|
-
CMDx::Correlator.id = "api-request-456"
|
483
|
-
ProcessApiRequestTask.call # Uses "api-request-456"
|
484
|
-
|
485
|
-
# 3. Existing chain ID when no explicit or thread correlation
|
486
|
-
task_with_run = ProcessOrderTask.call(chain: { id: "order-chain-789" })
|
487
|
-
# Uses "order-chain-789"
|
488
|
-
|
489
|
-
# 4. Generated UUID when none of the above exist
|
490
|
-
CMDx::Correlator.clear
|
491
|
-
ProcessOrderTask.call # Uses generated UUID
|
492
|
-
```
|
493
|
-
|
494
|
-
#### Explicit Correlation IDs
|
495
|
-
|
496
|
-
Set fixed or dynamic correlation IDs for specific tasks or workflows using strings, method names, or procs:
|
345
|
+
1. **Explicit correlation ID** (string, proc, method name)
|
346
|
+
2. **Thread-local correlation** (CMDx::Correlator.id)
|
347
|
+
3. **Existing chain ID** (inherited from parent task)
|
348
|
+
4. **Generated UUID** (when none exist)
|
497
349
|
|
498
350
|
```ruby
|
499
|
-
#
|
500
|
-
class
|
501
|
-
use :middleware, CMDx::Middlewares::Correlate, id: "
|
502
|
-
|
503
|
-
def call
|
504
|
-
# Always uses "payment-processing" as correlation ID
|
505
|
-
# Useful for grouping all payment operations
|
506
|
-
context.payment = PaymentService.charge(payment_params)
|
507
|
-
end
|
351
|
+
# Explicit correlation ID
|
352
|
+
class ProcessOrderTask < CMDx::Task
|
353
|
+
use :middleware, CMDx::Middlewares::Correlate, id: "order-processing"
|
508
354
|
end
|
509
355
|
|
510
|
-
# Dynamic correlation ID
|
356
|
+
# Dynamic correlation ID
|
511
357
|
class ProcessOrderTask < CMDx::Task
|
512
|
-
use :middleware, CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}
|
513
|
-
|
514
|
-
def call
|
515
|
-
# Dynamic correlation ID based on order and timestamp
|
516
|
-
# Each execution gets a unique correlation ID
|
517
|
-
ValidateOrderTask.call(context)
|
518
|
-
ProcessPaymentTask.call(context)
|
519
|
-
end
|
358
|
+
use :middleware, CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}" }
|
520
359
|
end
|
521
360
|
|
522
361
|
# Method-based correlation ID
|
523
362
|
class ProcessApiRequestTask < CMDx::Task
|
524
363
|
use :middleware, CMDx::Middlewares::Correlate, id: :generate_correlation_id
|
525
364
|
|
526
|
-
def call
|
527
|
-
# Uses correlation ID from generate_correlation_id method
|
528
|
-
context.api_response = ExternalService.call(request_data)
|
529
|
-
end
|
530
|
-
|
531
365
|
private
|
532
366
|
|
533
367
|
def generate_correlation_id
|
534
368
|
"api-#{context.request_id}-#{context.user_id}"
|
535
369
|
end
|
536
370
|
end
|
537
|
-
|
538
|
-
# Symbol fallback when method doesn't exist
|
539
|
-
class ProcessWorkflowTask < CMDx::Task
|
540
|
-
use :middleware, CMDx::Middlewares::Correlate, id: :workflow_processing
|
541
|
-
|
542
|
-
def call
|
543
|
-
# Uses :workflow_processing as correlation ID (symbol as-is)
|
544
|
-
# since task doesn't respond to workflow_processing method
|
545
|
-
context.workflow_results = process_workflow_items
|
546
|
-
end
|
547
|
-
end
|
548
|
-
```
|
549
|
-
|
550
|
-
#### Conditional Correlation
|
551
|
-
|
552
|
-
Apply correlation middleware conditionally based on environment or task state:
|
553
|
-
|
554
|
-
```ruby
|
555
|
-
class ProcessOrderTask < CMDx::Task
|
556
|
-
# Only apply correlation in production environments
|
557
|
-
use :middleware, CMDx::Middlewares::Correlate, unless: -> { Rails.env.development? }
|
558
|
-
|
559
|
-
def call
|
560
|
-
context.order = Order.find(order_id)
|
561
|
-
context.order.process!
|
562
|
-
end
|
563
|
-
end
|
564
|
-
|
565
|
-
class SendEmailTask < CMDx::Task
|
566
|
-
# Apply correlation only when tracing is enabled
|
567
|
-
use :middleware, CMDx::Middlewares::Correlate, if: :tracing_enabled?
|
568
|
-
|
569
|
-
def call
|
570
|
-
EmailService.deliver(email_params)
|
571
|
-
end
|
572
|
-
|
573
|
-
private
|
574
|
-
|
575
|
-
def tracing_enabled?
|
576
|
-
context.enable_tracing == true
|
577
|
-
end
|
578
|
-
end
|
579
|
-
```
|
580
|
-
|
581
|
-
#### Scoped Correlation Context
|
582
|
-
|
583
|
-
Use correlation blocks to establish correlation contexts for groups of related tasks:
|
584
|
-
|
585
|
-
```ruby
|
586
|
-
class ProcessOrderWorkflowTask < CMDx::Task
|
587
|
-
use :middleware, CMDx::Middlewares::Correlate
|
588
|
-
|
589
|
-
def call
|
590
|
-
# Establish correlation context for entire workflow
|
591
|
-
CMDx::Correlator.use("order-workflow-#{order_id}") do
|
592
|
-
ValidateOrderTask.call(context)
|
593
|
-
ProcessPaymentTask.call(context)
|
594
|
-
SendConfirmationTask.call(context)
|
595
|
-
UpdateInventoryTask.call(context)
|
596
|
-
end
|
597
|
-
end
|
598
|
-
end
|
599
|
-
```
|
600
|
-
|
601
|
-
#### Global Correlation Setup
|
602
|
-
|
603
|
-
Apply correlation middleware globally to all tasks:
|
604
|
-
|
605
|
-
```ruby
|
606
|
-
class ApplicationTask < CMDx::Task
|
607
|
-
use :middleware, CMDx::Middlewares::Correlate # All tasks get correlation management
|
608
|
-
end
|
609
|
-
|
610
|
-
class ProcessOrderTask < ApplicationTask
|
611
|
-
def call
|
612
|
-
# Automatically inherits correlation management
|
613
|
-
context.order = Order.find(order_id)
|
614
|
-
context.order.process!
|
615
|
-
end
|
616
|
-
end
|
617
371
|
```
|
618
372
|
|
619
373
|
#### Request Tracing Integration
|
620
374
|
|
621
|
-
Combine with request identifiers for comprehensive tracing:
|
622
|
-
|
623
375
|
```ruby
|
624
376
|
class ApiController < ApplicationController
|
625
377
|
before_action :set_correlation_id
|
626
378
|
|
627
379
|
def process_order
|
628
|
-
# Option 1: Use thread-local correlation (inherited by all tasks)
|
629
380
|
result = ProcessOrderTask.call(order_params)
|
630
381
|
|
631
|
-
# Option 2: Use explicit correlation ID for this specific request
|
632
|
-
# result = ProcessOrderTask.call(order_params.merge(correlation_id: @correlation_id))
|
633
|
-
|
634
382
|
if result.success?
|
635
383
|
render json: { order: result.context.order, correlation_id: result.chain.id }
|
636
384
|
else
|
@@ -641,9 +389,9 @@ class ApiController < ApplicationController
|
|
641
389
|
private
|
642
390
|
|
643
391
|
def set_correlation_id
|
644
|
-
|
645
|
-
CMDx::Correlator.id =
|
646
|
-
response.headers['X-Correlation-ID'] =
|
392
|
+
correlation_id = request.headers['X-Correlation-ID'] || request.uuid
|
393
|
+
CMDx::Correlator.id = correlation_id
|
394
|
+
response.headers['X-Correlation-ID'] = correlation_id
|
647
395
|
end
|
648
396
|
end
|
649
397
|
|
@@ -652,20 +400,6 @@ class ProcessOrderTask < CMDx::Task
|
|
652
400
|
|
653
401
|
def call
|
654
402
|
# Inherits correlation ID from controller thread context
|
655
|
-
# All subtasks will share the same correlation ID
|
656
|
-
ValidateOrderDataTask.call(context)
|
657
|
-
ChargePaymentTask.call(context)
|
658
|
-
SendConfirmationEmailTask.call(context)
|
659
|
-
end
|
660
|
-
end
|
661
|
-
|
662
|
-
# Alternative: Task-specific correlation for API endpoints
|
663
|
-
class ProcessApiOrderTask < CMDx::Task
|
664
|
-
use :middleware, CMDx::Middlewares::Correlate, id: -> { "api-order-#{context.request_id}" }
|
665
|
-
|
666
|
-
def call
|
667
|
-
# Uses correlation ID specific to this API request
|
668
|
-
# Overrides any thread-local correlation
|
669
403
|
ValidateOrderDataTask.call(context)
|
670
404
|
ChargePaymentTask.call(context)
|
671
405
|
SendConfirmationEmailTask.call(context)
|
@@ -673,15 +407,10 @@ class ProcessApiOrderTask < CMDx::Task
|
|
673
407
|
end
|
674
408
|
```
|
675
409
|
|
676
|
-
> [!TIP]
|
677
|
-
> The Correlate middleware integrates seamlessly with the CMDx logging system. All task execution logs automatically include the correlation ID, making it easy to trace related operations across your application.
|
678
|
-
|
679
|
-
> [!NOTE]
|
680
|
-
> Correlation IDs are thread-safe and automatically propagate through task hierarchies when using shared context. This makes the middleware ideal for distributed tracing and debugging complex business workflows.
|
681
|
-
|
682
410
|
## Writing Custom Middleware
|
683
411
|
|
684
|
-
|
412
|
+
> [!IMPORTANT]
|
413
|
+
> Custom middleware must inherit from `CMDx::Middleware` and implement the `call(task, callable)` method.
|
685
414
|
|
686
415
|
```ruby
|
687
416
|
class DatabaseTransactionMiddleware < CMDx::Middleware
|
@@ -697,26 +426,22 @@ class DatabaseTransactionMiddleware < CMDx::Middleware
|
|
697
426
|
end
|
698
427
|
end
|
699
428
|
|
700
|
-
class
|
701
|
-
def initialize(
|
702
|
-
@
|
703
|
-
@
|
429
|
+
class CacheMiddleware < CMDx::Middleware
|
430
|
+
def initialize(ttl: 300, key_prefix: nil)
|
431
|
+
@ttl = ttl
|
432
|
+
@key_prefix = key_prefix
|
704
433
|
end
|
705
434
|
|
706
435
|
def call(task, callable)
|
707
|
-
|
436
|
+
cache_key = build_cache_key(task)
|
437
|
+
cached_result = Rails.cache.read(cache_key)
|
708
438
|
|
709
|
-
if
|
710
|
-
task.fail!(reason: "Circuit breaker is open")
|
711
|
-
return task.result
|
712
|
-
end
|
439
|
+
return cached_result if cached_result
|
713
440
|
|
714
441
|
result = callable.call(task)
|
715
442
|
|
716
|
-
if result.
|
717
|
-
|
718
|
-
else
|
719
|
-
reset_circuit(circuit_key)
|
443
|
+
if result.success?
|
444
|
+
Rails.cache.write(cache_key, result, expires_in: @ttl)
|
720
445
|
end
|
721
446
|
|
722
447
|
result
|
@@ -724,21 +449,69 @@ class CircuitBreakerMiddleware < CMDx::Middleware
|
|
724
449
|
|
725
450
|
private
|
726
451
|
|
727
|
-
def
|
728
|
-
|
729
|
-
|
452
|
+
def build_cache_key(task)
|
453
|
+
base_key = task.class.name.underscore
|
454
|
+
param_hash = Digest::MD5.hexdigest(task.context.to_h.to_json)
|
455
|
+
[@key_prefix, base_key, param_hash].compact.join(':')
|
730
456
|
end
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
## Error Handling
|
461
|
+
|
462
|
+
> [!WARNING]
|
463
|
+
> Middleware errors can prevent task execution. Handle exceptions appropriately and consider their impact on the execution chain.
|
464
|
+
|
465
|
+
### Common Error Scenarios
|
466
|
+
|
467
|
+
```ruby
|
468
|
+
class ErrorProneMiddleware < CMDx::Middleware
|
469
|
+
def call(task, callable)
|
470
|
+
# Middleware error prevents task execution
|
471
|
+
raise "Configuration missing" unless configured?
|
731
472
|
|
732
|
-
|
733
|
-
|
473
|
+
callable.call(task)
|
474
|
+
rescue StandardError => e
|
475
|
+
# Handle middleware-specific errors
|
476
|
+
task.fail!(reason: "Middleware error: #{e.message}")
|
477
|
+
task.result
|
734
478
|
end
|
479
|
+
end
|
735
480
|
|
736
|
-
|
737
|
-
|
481
|
+
# Timeout errors are automatically handled
|
482
|
+
class ProcessOrderTask < CMDx::Task
|
483
|
+
use :middleware, CMDx::Middlewares::Timeout, seconds: 5
|
484
|
+
|
485
|
+
def call
|
486
|
+
sleep(10) # Exceeds timeout
|
738
487
|
end
|
739
488
|
end
|
489
|
+
|
490
|
+
result = ProcessOrderTask.call
|
491
|
+
result.failed? # → true
|
492
|
+
result.reason # → "Task timed out after 5 seconds"
|
740
493
|
```
|
741
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
|
510
|
+
```
|
511
|
+
|
512
|
+
> [!TIP]
|
513
|
+
> Design middleware to fail gracefully when possible. Consider whether middleware failure should prevent task execution or allow degraded operation.
|
514
|
+
|
742
515
|
---
|
743
516
|
|
744
517
|
- **Prev:** [Callbacks](callbacks.md)
|