cmdx 0.5.0 → 1.0.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 (151) 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 +19 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +95 -28
  7. data/README.md +73 -25
  8. data/docs/ai_prompts.md +319 -0
  9. data/docs/basics/call.md +234 -14
  10. data/docs/basics/chain.md +280 -0
  11. data/docs/basics/context.md +241 -33
  12. data/docs/basics/setup.md +85 -12
  13. data/docs/callbacks.md +283 -0
  14. data/docs/configuration.md +155 -30
  15. data/docs/getting_started.md +145 -22
  16. data/docs/internationalization.md +148 -0
  17. data/docs/interruptions/exceptions.md +198 -11
  18. data/docs/interruptions/faults.md +196 -44
  19. data/docs/interruptions/halt.md +188 -35
  20. data/docs/logging.md +204 -53
  21. data/docs/middlewares.md +745 -0
  22. data/docs/outcomes/result.md +305 -10
  23. data/docs/outcomes/states.md +212 -31
  24. data/docs/outcomes/statuses.md +284 -30
  25. data/docs/parameters/coercions.md +411 -29
  26. data/docs/parameters/defaults.md +258 -25
  27. data/docs/parameters/definitions.md +247 -72
  28. data/docs/parameters/namespacing.md +259 -27
  29. data/docs/parameters/validations.md +173 -168
  30. data/docs/testing.md +560 -0
  31. data/docs/tips_and_tricks.md +103 -42
  32. data/docs/workflows.md +329 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +69 -0
  35. data/lib/cmdx/callback_registry.rb +106 -0
  36. data/lib/cmdx/chain.rb +190 -0
  37. data/lib/cmdx/chain_inspector.rb +149 -0
  38. data/lib/cmdx/chain_serializer.rb +175 -0
  39. data/lib/cmdx/coercions/array.rb +37 -0
  40. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  41. data/lib/cmdx/coercions/boolean.rb +41 -1
  42. data/lib/cmdx/coercions/complex.rb +31 -0
  43. data/lib/cmdx/coercions/date.rb +39 -0
  44. data/lib/cmdx/coercions/date_time.rb +39 -0
  45. data/lib/cmdx/coercions/float.rb +31 -0
  46. data/lib/cmdx/coercions/hash.rb +42 -0
  47. data/lib/cmdx/coercions/integer.rb +32 -0
  48. data/lib/cmdx/coercions/rational.rb +31 -0
  49. data/lib/cmdx/coercions/string.rb +31 -0
  50. data/lib/cmdx/coercions/time.rb +39 -0
  51. data/lib/cmdx/coercions/virtual.rb +31 -0
  52. data/lib/cmdx/configuration.rb +217 -9
  53. data/lib/cmdx/context.rb +173 -2
  54. data/lib/cmdx/core_ext/hash.rb +72 -0
  55. data/lib/cmdx/core_ext/module.rb +94 -0
  56. data/lib/cmdx/core_ext/object.rb +105 -0
  57. data/lib/cmdx/correlator.rb +217 -0
  58. data/lib/cmdx/error.rb +210 -8
  59. data/lib/cmdx/errors.rb +256 -1
  60. data/lib/cmdx/fault.rb +177 -2
  61. data/lib/cmdx/faults.rb +158 -2
  62. data/lib/cmdx/immutator.rb +121 -2
  63. data/lib/cmdx/lazy_struct.rb +261 -18
  64. data/lib/cmdx/log_formatters/json.rb +46 -0
  65. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  66. data/lib/cmdx/log_formatters/line.rb +54 -0
  67. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  68. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  69. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  70. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  71. data/lib/cmdx/log_formatters/raw.rb +54 -0
  72. data/lib/cmdx/logger.rb +85 -0
  73. data/lib/cmdx/logger_ansi.rb +93 -7
  74. data/lib/cmdx/logger_serializer.rb +116 -0
  75. data/lib/cmdx/middleware.rb +74 -0
  76. data/lib/cmdx/middleware_registry.rb +106 -0
  77. data/lib/cmdx/middlewares/correlate.rb +266 -0
  78. data/lib/cmdx/middlewares/timeout.rb +232 -0
  79. data/lib/cmdx/parameter.rb +228 -1
  80. data/lib/cmdx/parameter_inspector.rb +61 -0
  81. data/lib/cmdx/parameter_registry.rb +125 -0
  82. data/lib/cmdx/parameter_serializer.rb +83 -0
  83. data/lib/cmdx/parameter_validator.rb +62 -0
  84. data/lib/cmdx/parameter_value.rb +109 -1
  85. data/lib/cmdx/parameters_inspector.rb +59 -0
  86. data/lib/cmdx/parameters_serializer.rb +102 -0
  87. data/lib/cmdx/railtie.rb +123 -3
  88. data/lib/cmdx/result.rb +367 -25
  89. data/lib/cmdx/result_ansi.rb +105 -9
  90. data/lib/cmdx/result_inspector.rb +76 -0
  91. data/lib/cmdx/result_logger.rb +90 -3
  92. data/lib/cmdx/result_serializer.rb +137 -0
  93. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  94. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  95. data/lib/cmdx/task.rb +405 -37
  96. data/lib/cmdx/task_serializer.rb +74 -2
  97. data/lib/cmdx/utils/ansi_color.rb +95 -0
  98. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  99. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  100. data/lib/cmdx/utils/name_affix.rb +78 -0
  101. data/lib/cmdx/validators/custom.rb +82 -0
  102. data/lib/cmdx/validators/exclusion.rb +94 -0
  103. data/lib/cmdx/validators/format.rb +102 -8
  104. data/lib/cmdx/validators/inclusion.rb +104 -0
  105. data/lib/cmdx/validators/length.rb +128 -0
  106. data/lib/cmdx/validators/numeric.rb +128 -0
  107. data/lib/cmdx/validators/presence.rb +93 -7
  108. data/lib/cmdx/version.rb +7 -1
  109. data/lib/cmdx/workflow.rb +394 -0
  110. data/lib/cmdx.rb +25 -64
  111. data/lib/generators/cmdx/install_generator.rb +37 -1
  112. data/lib/generators/cmdx/task_generator.rb +69 -1
  113. data/lib/generators/cmdx/templates/install.rb +43 -15
  114. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  115. data/lib/locales/ar.yml +36 -0
  116. data/lib/locales/cs.yml +36 -0
  117. data/lib/locales/da.yml +36 -0
  118. data/lib/locales/de.yml +36 -0
  119. data/lib/locales/el.yml +36 -0
  120. data/lib/locales/en.yml +20 -20
  121. data/lib/locales/es.yml +20 -20
  122. data/lib/locales/fi.yml +36 -0
  123. data/lib/locales/fr.yml +36 -0
  124. data/lib/locales/he.yml +36 -0
  125. data/lib/locales/hi.yml +36 -0
  126. data/lib/locales/it.yml +36 -0
  127. data/lib/locales/ja.yml +36 -0
  128. data/lib/locales/ko.yml +36 -0
  129. data/lib/locales/nl.yml +36 -0
  130. data/lib/locales/no.yml +36 -0
  131. data/lib/locales/pl.yml +36 -0
  132. data/lib/locales/pt.yml +36 -0
  133. data/lib/locales/ru.yml +36 -0
  134. data/lib/locales/sv.yml +36 -0
  135. data/lib/locales/th.yml +36 -0
  136. data/lib/locales/tr.yml +36 -0
  137. data/lib/locales/vi.yml +36 -0
  138. data/lib/locales/zh.yml +36 -0
  139. metadata +77 -15
  140. data/docs/basics/run.md +0 -34
  141. data/docs/batch.md +0 -53
  142. data/docs/example.md +0 -82
  143. data/docs/hooks.md +0 -62
  144. data/lib/cmdx/batch.rb +0 -43
  145. data/lib/cmdx/parameters.rb +0 -35
  146. data/lib/cmdx/run.rb +0 -39
  147. data/lib/cmdx/run_inspector.rb +0 -26
  148. data/lib/cmdx/run_serializer.rb +0 -20
  149. data/lib/cmdx/task_hook.rb +0 -18
  150. data/lib/generators/cmdx/batch_generator.rb +0 -30
  151. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -0,0 +1,745 @@
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
+ - [TLDR](#tldr)
8
+ - [Using Middleware](#using-middleware)
9
+ - [Class Middleware](#class-middleware)
10
+ - [Instance Middleware](#instance-middleware)
11
+ - [Proc Middleware](#proc-middleware)
12
+ - [Execution Order](#execution-order)
13
+ - [Short-circuiting](#short-circuiting)
14
+ - [Inheritance](#inheritance)
15
+ - [Built-in Middleware](#built-in-middleware)
16
+ - [Timeout Middleware](#timeout-middleware)
17
+ - [Correlate Middleware](#correlate-middleware)
18
+ - [Writing Custom Middleware](#writing-custom-middleware)
19
+
20
+ ## TLDR
21
+
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
28
+
29
+ ## Using Middleware
30
+
31
+ Declare middleware using the `use` method in your task classes:
32
+
33
+ ```ruby
34
+ class ProcessOrderTask < CMDx::Task
35
+ use AuthenticationMiddleware
36
+ use LoggingMiddleware, level: :info
37
+ use CachingMiddleware, ttl: 300
38
+
39
+ def call
40
+ context.order = Order.find(order_id)
41
+ context.order.process!
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### Class Middleware
47
+
48
+ The most common pattern - pass the middleware class with optional initialization arguments:
49
+
50
+ ```ruby
51
+ class AuditMiddleware < CMDx::Middleware
52
+ def initialize(action:, resource_type:)
53
+ @action = action
54
+ @resource_type = resource_type
55
+ end
56
+
57
+ def call(task, callable)
58
+ result = callable.call(task)
59
+
60
+ if result.success?
61
+ AuditLog.create!(
62
+ action: @action,
63
+ resource_type: @resource_type,
64
+ resource_id: task.context.id,
65
+ user_id: task.context.current_user.id
66
+ )
67
+ end
68
+
69
+ result
70
+ end
71
+ end
72
+
73
+ class ProcessOrderTask < CMDx::Task
74
+ use AuditMiddleware, action: 'process', resource_type: 'Order'
75
+
76
+ def call
77
+ context.order = Order.find(order_id)
78
+ context.order.process!
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### Instance Middleware
84
+
85
+ Pre-configured middleware instances for complex initialization:
86
+
87
+ ```ruby
88
+ class ProcessOrderTask < CMDx::Task
89
+ use LoggingMiddleware.new(
90
+ level: :debug,
91
+ formatter: JSON::JSONFormatter.new,
92
+ tags: ['order', 'payment']
93
+ )
94
+
95
+ def call
96
+ # Business logic
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### Proc Middleware
102
+
103
+ Inline middleware for simple cases:
104
+
105
+ ```ruby
106
+ class ProcessOrderTask < CMDx::Task
107
+ use proc { |task, callable|
108
+ start_time = Time.now
109
+ result = callable.call(task)
110
+ duration = Time.now - start_time
111
+
112
+ Rails.logger.info "#{task.class.name} completed in #{duration}s"
113
+ result
114
+ }
115
+
116
+ def call
117
+ # Business logic
118
+ end
119
+ end
120
+ ```
121
+
122
+ ## Execution Order
123
+
124
+ Middleware executes in nested fashion - first declared wraps all others:
125
+
126
+ ```ruby
127
+ class ProcessOrderTask < CMDx::Task
128
+ use TimingMiddleware # 1st: outermost
129
+ use AuthenticationMiddleware # 2nd: middle
130
+ use ValidationMiddleware # 3rd: innermost
131
+
132
+ def call
133
+ # Core logic executes last
134
+ end
135
+ end
136
+
137
+ # Execution flow:
138
+ # 1. TimingMiddleware before
139
+ # 2. AuthenticationMiddleware before
140
+ # 3. ValidationMiddleware before
141
+ # 4. [task execution]
142
+ # 5. ValidationMiddleware after
143
+ # 6. AuthenticationMiddleware after
144
+ # 7. TimingMiddleware after
145
+ ```
146
+
147
+ > [!IMPORTANT]
148
+ > Middleware executes in declaration order for setup and reverse order for cleanup, creating proper nesting.
149
+
150
+ ## Short-circuiting
151
+
152
+ Middleware can halt execution by not calling the next callable:
153
+
154
+ ```ruby
155
+ class RateLimitMiddleware < CMDx::Middleware
156
+ def initialize(limit: 100, window: 1.hour)
157
+ @limit = limit
158
+ @window = window
159
+ end
160
+
161
+ def call(task, callable)
162
+ key = "rate_limit:#{task.context.current_user.id}"
163
+ current_count = Rails.cache.read(key) || 0
164
+
165
+ if current_count >= @limit
166
+ task.fail!(reason: "Rate limit exceeded: #{@limit} requests per hour")
167
+ return task.result
168
+ end
169
+
170
+ Rails.cache.write(key, current_count + 1, expires_in: @window)
171
+ callable.call(task)
172
+ end
173
+ end
174
+
175
+ class SendEmailTask < CMDx::Task
176
+ use RateLimitMiddleware, limit: 50, window: 1.hour
177
+
178
+ def call
179
+ # Only executes if rate limit check passes
180
+ EmailService.deliver(
181
+ to: email_address,
182
+ subject: subject,
183
+ body: message_body
184
+ )
185
+ end
186
+ end
187
+ ```
188
+
189
+ ## Inheritance
190
+
191
+ Middleware is inherited from parent classes, enabling application-wide patterns:
192
+
193
+ ```ruby
194
+ class ApplicationTask < CMDx::Task
195
+ use RequestIdMiddleware # All tasks get request tracking
196
+ use PerformanceMiddleware # All tasks get performance monitoring
197
+ use ErrorReportingMiddleware # All tasks get error reporting
198
+ end
199
+
200
+ class ProcessOrderTask < ApplicationTask
201
+ use AuthenticationMiddleware # Specific to order processing
202
+ use OrderValidationMiddleware # Domain-specific validation
203
+
204
+ def call
205
+ # Inherits all ApplicationTask middleware plus order-specific ones
206
+ end
207
+ end
208
+ ```
209
+
210
+ > [!TIP]
211
+ > Middleware is inherited by subclasses, making it ideal for setting up global concerns across all tasks in your application.
212
+
213
+ ## Built-in Middleware
214
+
215
+ ### Timeout Middleware
216
+
217
+ Enforces execution time limits with support for static and dynamic timeout values.
218
+
219
+ #### Basic Usage
220
+
221
+ ```ruby
222
+ class ProcessLargeReportTask < CMDx::Task
223
+ use CMDx::Middlewares::Timeout, seconds: 300 # 5 minutes
224
+
225
+ def call
226
+ # Long-running report generation
227
+ end
228
+ end
229
+
230
+ # Default timeout (3 seconds when no value specified)
231
+ class QuickValidationTask < CMDx::Task
232
+ use CMDx::Middlewares::Timeout # Uses 3 seconds default
233
+
234
+ def call
235
+ # Fast validation logic
236
+ end
237
+ end
238
+ ```
239
+
240
+ #### Dynamic Timeout Generation
241
+
242
+ The middleware supports dynamic timeout calculation using method names, procs, and lambdas:
243
+
244
+ ```ruby
245
+ # Method-based timeout calculation
246
+ class ProcessOrderTask < CMDx::Task
247
+ use CMDx::Middlewares::Timeout, seconds: :calculate_timeout
248
+
249
+ def call
250
+ # Task execution with dynamic timeout
251
+ context.order = Order.find(order_id)
252
+ context.order.process!
253
+ end
254
+
255
+ private
256
+
257
+ def calculate_timeout
258
+ # Dynamic timeout based on order complexity
259
+ 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
262
+ base_timeout
263
+ end
264
+ end
265
+
266
+ # Proc-based timeout for inline calculation
267
+ class ProcessWorkflowTask < CMDx::Task
268
+ use CMDx::Middlewares::Timeout, seconds: -> {
269
+ context.workflow_size > 100 ? 120 : 60
270
+ }
271
+
272
+ def call
273
+ # Processes workflow with timeout based on size
274
+ context.workflow_items.each { |item| process_item(item) }
275
+ end
276
+ end
277
+
278
+ # Context-aware timeout calculation
279
+ class GenerateReportTask < CMDx::Task
280
+ use 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
+ ```
298
+
299
+ #### Timeout Precedence
300
+
301
+ The middleware follows this precedence for determining timeout values:
302
+
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
308
+
309
+ ```ruby
310
+ # Static timeout - highest precedence when specified
311
+ class ProcessOrderTask < CMDx::Task
312
+ use CMDx::Middlewares::Timeout, seconds: 45 # Always 45 seconds
313
+ end
314
+
315
+ # Method-based timeout - calls task method
316
+ class ProcessOrderTask < CMDx::Task
317
+ use 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
326
+ class ProcessOrderTask < CMDx::Task
327
+ use CMDx::Middlewares::Timeout, seconds: :might_return_nil
328
+
329
+ private
330
+ def might_return_nil
331
+ nil # Falls back to 3 seconds default
332
+ end
333
+ end
334
+ ```
335
+
336
+ #### Conditional Timeout
337
+
338
+ Apply timeout middleware conditionally based on environment or task state:
339
+
340
+ ```ruby
341
+ # Environment-based conditional timeout
342
+ class ProcessOrderTask < CMDx::Task
343
+ use CMDx::Middlewares::Timeout,
344
+ seconds: 60,
345
+ unless: -> { Rails.env.development? }
346
+
347
+ def call
348
+ # No timeout in development, 60 seconds in other environments
349
+ context.order = Order.find(order_id)
350
+ context.order.process!
351
+ end
352
+ end
353
+
354
+ # Context-based conditional timeout
355
+ class SendEmailTask < CMDx::Task
356
+ use CMDx::Middlewares::Timeout,
357
+ seconds: 30,
358
+ if: :timeout_enabled?
359
+
360
+ def call
361
+ EmailService.deliver(email_params)
362
+ end
363
+
364
+ private
365
+
366
+ def timeout_enabled?
367
+ !context.background_job?
368
+ end
369
+ end
370
+
371
+ # Combined dynamic timeout with conditions
372
+ class ProcessComplexOrderTask < CMDx::Task
373
+ use 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 CMDx::Middlewares::Timeout, seconds: 60 # Default 60 seconds for all tasks
403
+ end
404
+
405
+ class QuickTask < ApplicationTask
406
+ use 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 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
+ ```
427
+
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
+ ### Correlate Middleware
435
+
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.
437
+
438
+ ```ruby
439
+ class ProcessApiRequestTask < CMDx::Task
440
+ use CMDx::Middlewares::Correlate
441
+
442
+ def call
443
+ # Correlation ID is automatically managed
444
+ # Chain ID reflects the established correlation context
445
+ context.api_response = ExternalService.call(request_data)
446
+ end
447
+ end
448
+ ```
449
+
450
+ #### Correlation Precedence
451
+
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 CMDx::Middlewares::Correlate, id: "fixed-correlation-123"
460
+ end
461
+ ProcessOrderTask.call # Always uses "fixed-correlation-123"
462
+
463
+ # 1b. Dynamic proc/lambda ID
464
+ class ProcessOrderTask < CMDx::Task
465
+ use 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 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:
497
+
498
+ ```ruby
499
+ # Static string correlation ID
500
+ class ProcessPaymentTask < CMDx::Task
501
+ use 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
508
+ end
509
+
510
+ # Dynamic correlation ID using proc/lambda
511
+ class ProcessOrderTask < CMDx::Task
512
+ use 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
520
+ end
521
+
522
+ # Method-based correlation ID
523
+ class ProcessApiRequestTask < CMDx::Task
524
+ use CMDx::Middlewares::Correlate, id: :generate_correlation_id
525
+
526
+ def call
527
+ # Uses correlation ID from generate_correlation_id method
528
+ context.api_response = ExternalService.call(request_data)
529
+ end
530
+
531
+ private
532
+
533
+ def generate_correlation_id
534
+ "api-#{context.request_id}-#{context.user_id}"
535
+ end
536
+ end
537
+
538
+ # Symbol fallback when method doesn't exist
539
+ class ProcessWorkflowTask < CMDx::Task
540
+ use 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 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 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 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 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
+ ```
618
+
619
+ #### Request Tracing Integration
620
+
621
+ Combine with request identifiers for comprehensive tracing:
622
+
623
+ ```ruby
624
+ class ApiController < ApplicationController
625
+ before_action :set_correlation_id
626
+
627
+ def process_order
628
+ # Option 1: Use thread-local correlation (inherited by all tasks)
629
+ result = ProcessOrderTask.call(order_params)
630
+
631
+ # Option 2: Use explicit correlation ID for this specific request
632
+ # result = ProcessOrderTask.call(order_params.merge(correlation_id: @correlation_id))
633
+
634
+ if result.success?
635
+ render json: { order: result.context.order, correlation_id: result.chain.id }
636
+ else
637
+ render json: { error: result.reason }, status: 422
638
+ end
639
+ end
640
+
641
+ private
642
+
643
+ 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
647
+ end
648
+ end
649
+
650
+ class ProcessOrderTask < CMDx::Task
651
+ use CMDx::Middlewares::Correlate
652
+
653
+ def call
654
+ # 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 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
+ ValidateOrderDataTask.call(context)
670
+ ChargePaymentTask.call(context)
671
+ SendConfirmationEmailTask.call(context)
672
+ end
673
+ end
674
+ ```
675
+
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
+ ## Writing Custom Middleware
683
+
684
+ Inherit from `CMDx::Middleware` and implement the `call` method:
685
+
686
+ ```ruby
687
+ class DatabaseTransactionMiddleware < CMDx::Middleware
688
+ def call(task, callable)
689
+ ActiveRecord::Base.transaction do
690
+ result = callable.call(task)
691
+
692
+ # Rollback transaction if task failed
693
+ raise ActiveRecord::Rollback if result.failed?
694
+
695
+ result
696
+ end
697
+ end
698
+ end
699
+
700
+ class CircuitBreakerMiddleware < CMDx::Middleware
701
+ def initialize(failure_threshold: 5, reset_timeout: 60)
702
+ @failure_threshold = failure_threshold
703
+ @reset_timeout = reset_timeout
704
+ end
705
+
706
+ def call(task, callable)
707
+ circuit_key = "circuit:#{task.class.name}"
708
+
709
+ if circuit_open?(circuit_key)
710
+ task.fail!(reason: "Circuit breaker is open")
711
+ return task.result
712
+ end
713
+
714
+ result = callable.call(task)
715
+
716
+ if result.failed?
717
+ increment_failures(circuit_key)
718
+ else
719
+ reset_circuit(circuit_key)
720
+ end
721
+
722
+ result
723
+ end
724
+
725
+ private
726
+
727
+ def circuit_open?(key)
728
+ failures = Rails.cache.read("#{key}:failures") || 0
729
+ failures >= @failure_threshold
730
+ end
731
+
732
+ def increment_failures(key)
733
+ Rails.cache.increment("#{key}:failures", 1, expires_in: @reset_timeout)
734
+ end
735
+
736
+ def reset_circuit(key)
737
+ Rails.cache.delete("#{key}:failures")
738
+ end
739
+ end
740
+ ```
741
+
742
+ ---
743
+
744
+ - **Prev:** [Callbacks](callbacks.md)
745
+ - **Next:** [Workflows](workflows.md)