cmdx 0.5.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +31 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +203 -31
- data/docs/outcomes/statuses.md +275 -30
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/middlewares.md
ADDED
@@ -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)
|