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.
- 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 +42 -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 +212 -19
- data/docs/outcomes/statuses.md +284 -18
- 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 +399 -20
- 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 +409 -34
- 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 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- 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/basics/call.md
CHANGED
@@ -1,31 +1,242 @@
|
|
1
1
|
# Basics - Call
|
2
2
|
|
3
|
-
Calling a task executes the logic within it. Tasks
|
4
|
-
the `call` and `call!` class methods.
|
3
|
+
Calling a task executes the business logic within it. Tasks provide two execution methods that handle success and failure scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow.
|
5
4
|
|
6
|
-
##
|
5
|
+
## Table of Contents
|
7
6
|
|
8
|
-
|
7
|
+
- [Execution Methods Overview](#execution-methods-overview)
|
8
|
+
- [Non-bang Call (`call`)](#non-bang-call-call)
|
9
|
+
- [Bang Call (`call!`)](#bang-call-call)
|
10
|
+
- [Direct Instantiation](#direct-instantiation)
|
11
|
+
- [Parameter Passing](#parameter-passing)
|
12
|
+
- [Result Propagation (`throw!`)](#result-propagation-throw)
|
13
|
+
- [Result Callbacks](#result-callbacks)
|
14
|
+
- [Task State Lifecycle](#task-state-lifecycle)
|
15
|
+
- [Return Value Details](#return-value-details)
|
16
|
+
|
17
|
+
## Execution Methods Overview
|
18
|
+
|
19
|
+
| Method | Returns | Exceptions | Use Case |
|
20
|
+
|--------|---------|------------|----------|
|
21
|
+
| `call` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
|
22
|
+
| `call!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` on failure/skip | Exception-based control flow |
|
23
|
+
|
24
|
+
## Non-bang Call (`call`)
|
25
|
+
|
26
|
+
The `call` method always returns a `CMDx::Result` object regardless of execution outcome. This is the preferred method for most use cases.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
result = ProcessOrderTask.call(order_id: 12345)
|
30
|
+
|
31
|
+
# Check execution state
|
32
|
+
result.success? #=> true/false
|
33
|
+
result.failed? #=> true/false
|
34
|
+
result.skipped? #=> true/false
|
35
|
+
|
36
|
+
# Access result data
|
37
|
+
result.context.order_id #=> 12345
|
38
|
+
result.runtime #=> 0.05 (seconds)
|
39
|
+
result.state #=> "complete"
|
40
|
+
result.status #=> "success"
|
41
|
+
```
|
42
|
+
|
43
|
+
### Handling Different Outcomes
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
result = ProcessOrderTask.call(order_id: 12345)
|
47
|
+
|
48
|
+
case result.status
|
49
|
+
when "success"
|
50
|
+
puts "Order processed: #{result.context.order_id}"
|
51
|
+
when "skipped"
|
52
|
+
puts "Order skipped: #{result.metadata[:reason]}"
|
53
|
+
when "failed"
|
54
|
+
puts "Order failed: #{result.metadata[:reason]}"
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
## Bang Call (`call!`)
|
59
|
+
|
60
|
+
The bang `call!` method raises a `CMDx::Fault` exception when tasks fail or are skipped, based on the `task_halt` configuration. It returns a `CMDx::Result` object only on success. This method is useful in scenarios where you want exception-based control flow.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
begin
|
64
|
+
result = ProcessOrderTask.call!(order_id: 12345)
|
65
|
+
puts "Order processed: #{result.context.order_id}"
|
66
|
+
rescue CMDx::Failed => e
|
67
|
+
# Handle failure
|
68
|
+
RetryOrderJob.perform_later(e.result.context.order_id)
|
69
|
+
rescue CMDx::Skipped => e
|
70
|
+
# Handle skip
|
71
|
+
Rails.logger.info("Order skipped: #{e.result.metadata[:reason]}")
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
### Exception Types
|
76
|
+
|
77
|
+
| Exception | Raised When | Purpose |
|
78
|
+
|-----------|-------------|---------|
|
79
|
+
| `CMDx::Failed` | Task execution fails | Handle failure scenarios |
|
80
|
+
| `CMDx::Skipped` | Task execution is skipped | Handle skip scenarios |
|
81
|
+
|
82
|
+
## Direct Instantiation
|
83
|
+
|
84
|
+
Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Direct instantiation
|
88
|
+
task = ProcessOrderTask.new(order_id: 12345, notify_customer: true)
|
89
|
+
|
90
|
+
# Access properties before execution
|
91
|
+
task.id #=> "abc123..." (unique task ID)
|
92
|
+
task.context.order_id #=> 12345
|
93
|
+
task.context.notify_customer #=> true
|
94
|
+
task.result.state #=> "initialized"
|
95
|
+
|
96
|
+
# Manual execution
|
97
|
+
task.perform
|
98
|
+
task.result.success? #=> true/false
|
99
|
+
```
|
100
|
+
|
101
|
+
### Execution Approaches
|
102
|
+
|
103
|
+
| Approach | Use Case | Benefits |
|
104
|
+
|----------|----------|----------|
|
105
|
+
| `TaskClass.call(...)` | Standard execution | Simple, handles full lifecycle |
|
106
|
+
| `TaskClass.call!(...)` | Exception-based flow | Automatic fault raising |
|
107
|
+
| `TaskClass.new(...).perform` | Advanced scenarios | Full control, testing flexibility |
|
108
|
+
|
109
|
+
> [!NOTE]
|
110
|
+
> Direct instantiation gives you access to the task instance before and after execution, but you must call the execution method manually.
|
111
|
+
|
112
|
+
## Parameter Passing
|
113
|
+
|
114
|
+
All methods accept parameters that become available in the task context:
|
9
115
|
|
10
116
|
```ruby
|
11
|
-
|
117
|
+
# Direct parameters
|
118
|
+
result = ProcessOrderTask.call(
|
119
|
+
order_id: 12345,
|
120
|
+
notify_customer: true,
|
121
|
+
priority: "high"
|
122
|
+
)
|
123
|
+
|
124
|
+
# From another task result
|
125
|
+
validation_result = ValidateOrderTask.call(order_id: 12345)
|
126
|
+
|
127
|
+
# -- Passing Result object
|
128
|
+
result = ProcessOrderTask.call!(validation_result)
|
129
|
+
|
130
|
+
# -- Passing context directly
|
131
|
+
result = ProcessOrderTask.new(validation_result.context)
|
12
132
|
```
|
13
133
|
|
14
|
-
##
|
134
|
+
## Result Propagation (`throw!`)
|
15
135
|
|
16
|
-
The
|
17
|
-
`task_halt` status options, otherwise it will return a `CMDx::Result` object. This
|
18
|
-
form of call is useful in background jobs where retries are done via the exception mechanism.
|
136
|
+
The `throw!` method enables result propagation, allowing tasks to bubble up failures from subtasks while preserving the original fault information:
|
19
137
|
|
20
138
|
```ruby
|
21
|
-
ProcessOrderTask
|
139
|
+
class ProcessOrderTask < CMDx::Task
|
140
|
+
def call
|
141
|
+
validation_result = ValidateOrderTask.call(context)
|
142
|
+
throw!(validation_result) if validation_result.failed?
|
143
|
+
|
144
|
+
payment_result = ProcessPaymentTask.call(context)
|
145
|
+
throw!(payment_result) if payment_result.skipped?
|
146
|
+
|
147
|
+
delivery_result = ScheduleDeliveryTask.call(context)
|
148
|
+
throw!(delivery_result) # failed or skipped
|
149
|
+
|
150
|
+
# Continue with main logic
|
151
|
+
context.order = Order.find(context.order_id)
|
152
|
+
finalize_order_processing
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
## Result Callbacks
|
158
|
+
|
159
|
+
Results support fluent callback patterns for conditional logic:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
ProcessOrderTask
|
163
|
+
.call(order_id: 12345)
|
164
|
+
.on_success { |result|
|
165
|
+
SendOrderConfirmationTask.call(result.context)
|
166
|
+
}
|
167
|
+
.on_failed { |result|
|
168
|
+
Honeybadger.notify(result.metadata[:error])
|
169
|
+
}
|
170
|
+
.on_executed { |result|
|
171
|
+
StatsD.timing('order.processing_time', result.runtime)
|
172
|
+
}
|
173
|
+
```
|
174
|
+
|
175
|
+
### Available Callbacks
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
result = ProcessOrderTask.call(order_id: 12345)
|
179
|
+
|
180
|
+
# State-based callbacks
|
181
|
+
result
|
182
|
+
.on_complete { |r| cleanup_resources(r) }
|
183
|
+
.on_interrupted { |r| handle_interruption(r) }
|
184
|
+
.on_executed { |r| log_execution_time(r) }
|
185
|
+
|
186
|
+
# Status-based callbacks
|
187
|
+
result
|
188
|
+
.on_success { |r| handle_success(r) }
|
189
|
+
.on_skipped { |r| handle_skip(r) }
|
190
|
+
.on_failed { |r| handle_failure(r) }
|
191
|
+
|
192
|
+
# Outcome-based callbacks
|
193
|
+
result
|
194
|
+
.on_good { |r| log_positive_outcome(r) }
|
195
|
+
.on_bad { |r| log_negative_outcome(r) }
|
196
|
+
```
|
197
|
+
|
198
|
+
## Task State Lifecycle
|
199
|
+
|
200
|
+
Tasks progress through defined states during execution:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
result = ProcessOrderTask.call(order_id: 12345)
|
204
|
+
|
205
|
+
# Execution states
|
206
|
+
result.state #=> "initialized" -> "executing" -> "complete"/"interrupted"
|
207
|
+
|
208
|
+
# Outcome statuses
|
209
|
+
result.status #=> "success"/"failed"/"skipped"
|
210
|
+
```
|
211
|
+
|
212
|
+
## Return Value Details
|
213
|
+
|
214
|
+
The `Result` object provides comprehensive execution information:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
result = ProcessOrderTask.call(order_id: 12345)
|
218
|
+
|
219
|
+
# Execution metadata
|
220
|
+
result.id #=> "abc123..." (unique execution ID)
|
221
|
+
result.runtime #=> 0.05 (execution time in seconds)
|
222
|
+
result.task #=> ProcessOrderTask instance
|
223
|
+
result.chain #=> Chain object for tracking executions
|
224
|
+
|
225
|
+
# Context and metadata
|
226
|
+
result.context #=> Context with all task data
|
227
|
+
result.metadata #=> Hash with execution metadata
|
228
|
+
|
229
|
+
# State checking methods
|
230
|
+
result.good? #=> true for success/skipped
|
231
|
+
result.bad? #=> true for failed/skipped
|
232
|
+
result.complete? #=> true when execution finished
|
233
|
+
result.interrupted? #=> true for failed/skipped
|
22
234
|
```
|
23
235
|
|
24
236
|
> [!IMPORTANT]
|
25
|
-
> Tasks are single
|
26
|
-
> as result object will be returned. Build a new task call to execute a new instance of the same task.
|
237
|
+
> Tasks are single-use objects. Once executed, they are frozen and cannot be called again. Create a new task instance to execute the same task again.
|
27
238
|
|
28
239
|
---
|
29
240
|
|
30
|
-
- **Prev:** [Basics - Setup](
|
31
|
-
- **Next:** [Basics - Context](
|
241
|
+
- **Prev:** [Basics - Setup](setup.md)
|
242
|
+
- **Next:** [Basics - Context](context.md)
|
@@ -0,0 +1,271 @@
|
|
1
|
+
# Basics - Chain
|
2
|
+
|
3
|
+
A chain represents a collection of related task executions that share a common execution context. Chains provide unified tracking, indexing, and reporting for task workflows using thread-local storage to automatically group related tasks without manual coordination.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Thread-Local Chain Management](#thread-local-chain-management)
|
8
|
+
- [Automatic Chain Creation](#automatic-chain-creation)
|
9
|
+
- [Chain Inheritance](#chain-inheritance)
|
10
|
+
- [Chain Structure and Metadata](#chain-structure-and-metadata)
|
11
|
+
- [Correlation ID Integration](#correlation-id-integration)
|
12
|
+
- [State Delegation](#state-delegation)
|
13
|
+
- [Serialization and Logging](#serialization-and-logging)
|
14
|
+
|
15
|
+
## Thread-Local Chain Management
|
16
|
+
|
17
|
+
Chains use thread-local storage to automatically group related task executions within the same thread while maintaining isolation across different threads:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# Each thread gets its own chain context
|
21
|
+
Thread.new do
|
22
|
+
result = ProcessOrderTask.call(order_id: 123)
|
23
|
+
result.chain.id # => unique ID for this thread
|
24
|
+
end
|
25
|
+
|
26
|
+
Thread.new do
|
27
|
+
result = ProcessOrderTask.call(order_id: 456)
|
28
|
+
result.chain.id # => different unique ID
|
29
|
+
end
|
30
|
+
|
31
|
+
# Access the current thread's chain
|
32
|
+
CMDx::Chain.current # => current chain or nil
|
33
|
+
CMDx::Chain.clear # => clears current thread's chain
|
34
|
+
```
|
35
|
+
|
36
|
+
## Automatic Chain Creation
|
37
|
+
|
38
|
+
Every task execution automatically creates or joins a thread-local chain context:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# Single task creates its own chain
|
42
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
43
|
+
result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
44
|
+
result.chain.results.size #=> 1
|
45
|
+
|
46
|
+
# Subsequent tasks in the same thread join the existing chain
|
47
|
+
result2 = AnotherTask.call(data: "test")
|
48
|
+
result2.chain.id == result.chain.id #=> true
|
49
|
+
result2.chain.results.size #=> 2
|
50
|
+
```
|
51
|
+
|
52
|
+
## Chain Inheritance
|
53
|
+
|
54
|
+
When tasks call other tasks within the same thread, they automatically inherit the current chain, creating a cohesive execution trail:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class ProcessUserOrderTask < CMDx::Task
|
58
|
+
def call
|
59
|
+
context.order = Order.find(order_id)
|
60
|
+
|
61
|
+
# Subtasks automatically inherit the current thread's chain
|
62
|
+
SendOrderConfirmationTask.call(order_id: order_id)
|
63
|
+
NotifyWarehousePartnersTask.call(order_id: order_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
68
|
+
chain = result.chain
|
69
|
+
|
70
|
+
# All related tasks share the same chain automatically
|
71
|
+
chain.results.size #=> 3
|
72
|
+
chain.results.map(&:task).map(&:class)
|
73
|
+
#=> [ProcessUserOrderTask, SendOrderConfirmationTask, NotifyWarehousePartnersTask]
|
74
|
+
```
|
75
|
+
|
76
|
+
> [!NOTE]
|
77
|
+
> Tasks automatically inherit the current thread's chain, creating a unified execution trail for debugging and monitoring purposes without any manual chain management.
|
78
|
+
|
79
|
+
## Chain Structure and Metadata
|
80
|
+
|
81
|
+
Chains provide comprehensive execution information:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
85
|
+
chain = result.chain
|
86
|
+
|
87
|
+
# Chain identification
|
88
|
+
chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
89
|
+
chain.results #=> [<CMDx::Result ...>, <CMDx::Result ...>]
|
90
|
+
|
91
|
+
# Execution state (delegates to outer most result)
|
92
|
+
chain.state #=> "complete"
|
93
|
+
chain.status #=> "success"
|
94
|
+
chain.outcome #=> "success"
|
95
|
+
chain.runtime #=> 0.5
|
96
|
+
```
|
97
|
+
|
98
|
+
## Correlation ID Integration
|
99
|
+
|
100
|
+
Chains automatically integrate with the correlation tracking system through thread-local storage, providing seamless request tracing across task boundaries. The chain ID serves as the correlation identifier, enabling you to trace execution flows through distributed systems and complex business logic.
|
101
|
+
|
102
|
+
### Custom Chain IDs
|
103
|
+
|
104
|
+
You can specify custom chain IDs for specific correlation contexts:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# Create a chain with custom ID
|
108
|
+
chain = CMDx::Chain.new(id: "user-session-123")
|
109
|
+
CMDx::Chain.current = chain
|
110
|
+
|
111
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
112
|
+
result.chain.id #=> "user-session-123"
|
113
|
+
```
|
114
|
+
|
115
|
+
### Automatic Correlation Inheritance
|
116
|
+
|
117
|
+
Chains inherit correlation IDs using a hierarchical precedence system:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# 1. Existing chain ID takes precedence
|
121
|
+
CMDx::Chain.current = CMDx::Chain.new(id: "custom-correlation-123")
|
122
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
123
|
+
result.chain.id #=> "custom-correlation-123"
|
124
|
+
|
125
|
+
# 2. Thread-local correlation ID is used if no chain exists
|
126
|
+
CMDx::Chain.clear
|
127
|
+
CMDx::Correlator.id = "thread-correlation-456"
|
128
|
+
result = ProcessUserOrderTask.call
|
129
|
+
result.chain.id #=> "thread-correlation-456"
|
130
|
+
|
131
|
+
# 3. Generated UUID when no correlation exists
|
132
|
+
CMDx::Correlator.clear
|
133
|
+
result = ProcessUserOrderTask.call
|
134
|
+
result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5" (generated)
|
135
|
+
```
|
136
|
+
|
137
|
+
### Cross-Task Correlation Propagation
|
138
|
+
|
139
|
+
When tasks call subtasks within the same thread, correlation IDs automatically propagate:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class ProcessUserOrderTask < CMDx::Task
|
143
|
+
def call
|
144
|
+
context.order = Order.find(order_id)
|
145
|
+
|
146
|
+
# Subtasks inherit the same correlation ID automatically
|
147
|
+
SendOrderConfirmationTask.call(order_id: order_id)
|
148
|
+
NotifyWarehousePartnersTask.call(order_id: order_id)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Set correlation for this execution context
|
153
|
+
CMDx::Chain.current = CMDx::Chain.new(id: "user-order-correlation-123")
|
154
|
+
|
155
|
+
result = ProcessUserOrderTask.call(order_id: 456)
|
156
|
+
chain = result.chain
|
157
|
+
|
158
|
+
# All tasks share the same correlation ID
|
159
|
+
chain.id #=> "user-order-correlation-123"
|
160
|
+
chain.results.all? { |r| r.chain.id == "user-order-correlation-123" } #=> true
|
161
|
+
```
|
162
|
+
|
163
|
+
### Correlation Context Management
|
164
|
+
|
165
|
+
Use correlation blocks to manage correlation scope:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Correlation applies only within the block
|
169
|
+
CMDx::Correlator.use("api-request-789") do
|
170
|
+
result = ProcessApiRequestTask.call(request_data: data)
|
171
|
+
result.chain.id #=> "api-request-789"
|
172
|
+
|
173
|
+
# Nested task calls inherit the same correlation
|
174
|
+
AuditLogTask.call(audit_data: data)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Outside the block, correlation context is restored
|
178
|
+
result = AnotherTask.call
|
179
|
+
result.chain.id #=> different correlation ID
|
180
|
+
```
|
181
|
+
|
182
|
+
### Middleware Integration
|
183
|
+
|
184
|
+
The `CMDx::Middlewares::Correlate` middleware automatically manages correlation contexts during task execution:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class ProcessOrderTask < CMDx::Task
|
188
|
+
# Apply correlate middleware globally or per-task
|
189
|
+
use CMDx::Middlewares::Correlate
|
190
|
+
|
191
|
+
def call
|
192
|
+
# Correlation is automatically managed
|
193
|
+
# Chain ID reflects the established correlation context
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
> [!TIP]
|
199
|
+
> Chain IDs serve as correlation identifiers, making it easy to trace related operations across your application. The thread-local storage ensures automatic correlation without manual chain management.
|
200
|
+
|
201
|
+
> [!NOTE]
|
202
|
+
> Correlation IDs are particularly useful for debugging distributed systems, API request tracing, and understanding complex business workflows. All logs and results automatically include the chain ID for correlation.
|
203
|
+
|
204
|
+
## State Delegation
|
205
|
+
|
206
|
+
Chain state information delegates to the first (outer most) result, representing the overall execution outcome:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
class ProcessOrderTask < CMDx::Task
|
210
|
+
def call
|
211
|
+
ValidateOrderDataTask.call!(order_id: order_id) # Success
|
212
|
+
ProcessOrderPaymentTask.call!(order_id: order_id) # Failed
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
result = ProcessOrderTask.call
|
217
|
+
chain = result.chain
|
218
|
+
|
219
|
+
# Chain status reflects the main task, not subtasks
|
220
|
+
chain.status #=> "failed" (ProcessOrderPaymentTask failed)
|
221
|
+
chain.state #=> "interrupted"
|
222
|
+
|
223
|
+
# Individual task results maintain their own state
|
224
|
+
chain.results[0].status #=> "failed" (ProcessOrderTask)
|
225
|
+
chain.results[1].status #=> "success" (ValidateOrderDataTask)
|
226
|
+
chain.results[2].status #=> "failed" (ProcessOrderPaymentTask)
|
227
|
+
```
|
228
|
+
|
229
|
+
> [!IMPORTANT]
|
230
|
+
> Chain state always reflects the first (outer most) task outcome, not the subtasks. Individual subtask results maintain their own success/failure states.
|
231
|
+
|
232
|
+
## Serialization and Logging
|
233
|
+
|
234
|
+
Chains provide comprehensive serialization capabilities for monitoring and debugging:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
238
|
+
chain = result.chain
|
239
|
+
|
240
|
+
# Hash representation with all execution data
|
241
|
+
chain.to_h
|
242
|
+
#=> {
|
243
|
+
# id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
244
|
+
# state: "complete",
|
245
|
+
# status: "success",
|
246
|
+
# outcome: "success",
|
247
|
+
# runtime: 0.5,
|
248
|
+
# results: [
|
249
|
+
# { class: "ProcessUserOrderTask", state: "complete", status: "success", ... },
|
250
|
+
# { class: "SendOrderConfirmationTask", state: "complete", status: "success", ... },
|
251
|
+
# { class: "NotifyWarehousePartnersTask", state: "complete", status: "success", ... }
|
252
|
+
# ]
|
253
|
+
# }
|
254
|
+
|
255
|
+
# Human-readable summary
|
256
|
+
puts chain.to_s
|
257
|
+
# chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
258
|
+
# ================================================
|
259
|
+
#
|
260
|
+
# ProcessUserOrderTask: index=0 state=complete status=success ...
|
261
|
+
# SendOrderConfirmationTask: index=1 state=complete status=success ...
|
262
|
+
# NotifyWarehousePartnersTask: index=2 state=complete status=success ...
|
263
|
+
#
|
264
|
+
# ================================================
|
265
|
+
# state: complete | status: success | outcome: success | runtime: 0.5
|
266
|
+
```
|
267
|
+
|
268
|
+
---
|
269
|
+
|
270
|
+
- **Prev:** [Basics - Context](context.md)
|
271
|
+
- **Next:** [Interruptions - Halt](../interruptions/halt.md)
|