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