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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. 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
- - **Purpose** - Rack-style wrappers for cross-cutting concerns (auth, logging, caching)
23
- - **Declaration** - Use `use` method with classes, instances, or procs
24
- - **Execution order** - Nested fashion (first declared wraps all others)
25
- - **Short-circuiting** - Middleware can halt execution by not calling next callable
26
- - **Inheritance** - Middleware is inherited from parent classes
27
- - **Built-in** - Includes Timeout and Correlate middleware
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
- Declare middleware using the `use` method in your task classes:
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.id
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: JSON::JSONFormatter.new,
88
+ formatter: CustomFormatter.new,
92
89
  tags: ['order', 'payment']
93
90
  )
94
91
 
95
92
  def call
96
- # Business logic
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
- Middleware executes in nested fashion - first declared wraps all others:
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 last
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
- Middleware can halt execution by not calling the next callable:
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.id}"
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, window: 1.hour
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
- Middleware is inherited from parent classes, enabling application-wide patterns:
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 # Specific to order processing
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 # 5 minutes
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 when no value specified)
224
+ # Default timeout (3 seconds)
231
225
  class QuickValidationTask < CMDx::Task
232
- use :middleware, CMDx::Middlewares::Timeout # Uses 3 seconds default
226
+ use :middleware, CMDx::Middlewares::Timeout
233
227
 
234
228
  def call
235
- # Fast validation logic
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 Generation
235
+ #### Dynamic Timeout Calculation
241
236
 
242
- The middleware supports dynamic timeout calculation using method names, procs, and lambdas:
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 calculation
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) # 2 seconds per item
261
- base_timeout += 60 if context.payment_method == "bank_transfer" # Extra time for bank transfers
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 for inline calculation
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 follows this precedence for determining timeout values:
274
+ The middleware determines timeout values using this precedence:
302
275
 
303
- 1. **Explicit timeout value** (provided during middleware initialization)
304
- - Integer/Float: Used as-is for static timeout
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 - highest precedence when specified
280
+ # Static timeout - always 45 seconds
311
281
  class ProcessOrderTask < CMDx::Task
312
- use :middleware, CMDx::Middlewares::Timeout, seconds: 45 # Always 45 seconds
282
+ use :middleware, CMDx::Middlewares::Timeout, seconds: 45
313
283
  end
314
284
 
315
- # Method-based timeout - calls task 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 # Falls back to 3 seconds default
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 conditional timeout
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 conditional timeout
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
- Manages correlation IDs for request tracing across task boundaries. This middleware automatically establishes correlation contexts during task execution, enabling you to trace related operations through distributed systems and complex business workflows.
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 is automatically managed
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 follows a hierarchical precedence system for determining correlation IDs:
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
- # 1b. Dynamic proc/lambda ID
464
- class ProcessOrderTask < CMDx::Task
465
- use :middleware, CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}-#{rand(1000)}" }
466
- end
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
- # Static string correlation ID
500
- class ProcessPaymentTask < CMDx::Task
501
- use :middleware, CMDx::Middlewares::Correlate, id: "payment-processing"
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 using proc/lambda
356
+ # Dynamic correlation ID
511
357
  class ProcessOrderTask < CMDx::Task
512
- use :middleware, CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}-#{Time.now.to_i}" }
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
- @correlation_id = request.headers['X-Correlation-ID'] || request.uuid
645
- CMDx::Correlator.id = @correlation_id
646
- response.headers['X-Correlation-ID'] = @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
- Inherit from `CMDx::Middleware` and implement the `call` method:
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 CircuitBreakerMiddleware < CMDx::Middleware
701
- def initialize(failure_threshold: 5, reset_timeout: 60)
702
- @failure_threshold = failure_threshold
703
- @reset_timeout = reset_timeout
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
- circuit_key = "circuit:#{task.class.name}"
436
+ cache_key = build_cache_key(task)
437
+ cached_result = Rails.cache.read(cache_key)
708
438
 
709
- if circuit_open?(circuit_key)
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.failed?
717
- increment_failures(circuit_key)
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 circuit_open?(key)
728
- failures = Rails.cache.read("#{key}:failures") || 0
729
- failures >= @failure_threshold
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
- def increment_failures(key)
733
- Rails.cache.increment("#{key}:failures", 1, expires_in: @reset_timeout)
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
- def reset_circuit(key)
737
- Rails.cache.delete("#{key}:failures")
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)