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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +19 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +95 -28
- data/README.md +73 -25
- data/docs/ai_prompts.md +319 -0
- data/docs/basics/call.md +234 -14
- data/docs/basics/chain.md +280 -0
- data/docs/basics/context.md +241 -33
- data/docs/basics/setup.md +85 -12
- data/docs/callbacks.md +283 -0
- data/docs/configuration.md +155 -30
- data/docs/getting_started.md +145 -22
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +198 -11
- data/docs/interruptions/faults.md +196 -44
- data/docs/interruptions/halt.md +188 -35
- data/docs/logging.md +204 -53
- data/docs/middlewares.md +745 -0
- data/docs/outcomes/result.md +305 -10
- data/docs/outcomes/states.md +212 -31
- data/docs/outcomes/statuses.md +284 -30
- data/docs/parameters/coercions.md +411 -29
- data/docs/parameters/defaults.md +258 -25
- data/docs/parameters/definitions.md +247 -72
- data/docs/parameters/namespacing.md +259 -27
- data/docs/parameters/validations.md +173 -168
- data/docs/testing.md +560 -0
- data/docs/tips_and_tricks.md +103 -42
- data/docs/workflows.md +329 -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 +43 -15
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- data/lib/locales/ar.yml +36 -0
- data/lib/locales/cs.yml +36 -0
- data/lib/locales/da.yml +36 -0
- data/lib/locales/de.yml +36 -0
- data/lib/locales/el.yml +36 -0
- data/lib/locales/en.yml +20 -20
- data/lib/locales/es.yml +20 -20
- data/lib/locales/fi.yml +36 -0
- data/lib/locales/fr.yml +36 -0
- data/lib/locales/he.yml +36 -0
- data/lib/locales/hi.yml +36 -0
- data/lib/locales/it.yml +36 -0
- data/lib/locales/ja.yml +36 -0
- data/lib/locales/ko.yml +36 -0
- data/lib/locales/nl.yml +36 -0
- data/lib/locales/no.yml +36 -0
- data/lib/locales/pl.yml +36 -0
- data/lib/locales/pt.yml +36 -0
- data/lib/locales/ru.yml +36 -0
- data/lib/locales/sv.yml +36 -0
- data/lib/locales/th.yml +36 -0
- data/lib/locales/tr.yml +36 -0
- data/lib/locales/vi.yml +36 -0
- data/lib/locales/zh.yml +36 -0
- metadata +77 -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
@@ -1,29 +1,216 @@
|
|
1
1
|
# Interruptions - Exceptions
|
2
2
|
|
3
|
-
|
3
|
+
CMDx provides robust exception handling that differs between the `call` and `call!`
|
4
|
+
methods. Understanding how unhandled exceptions are processed is crucial for
|
5
|
+
building reliable task execution flows and implementing proper error handling strategies.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
+
## Table of Contents
|
8
|
+
|
9
|
+
- [TLDR](#tldr)
|
10
|
+
- [Exception Handling Behavior](#exception-handling-behavior)
|
11
|
+
- [Bang Call (`call!`)](#bang-call-call)
|
12
|
+
- [Exception Classification](#exception-classification)
|
13
|
+
|
14
|
+
## TLDR
|
15
|
+
|
16
|
+
- **`call`** - Captures ALL exceptions, converts to failed results with metadata
|
17
|
+
- **`call!`** - Lets exceptions propagate (except CMDx faults based on task_halt config)
|
18
|
+
- **Exception info** - Available in `result.metadata[:original_exception]` and `result.metadata[:reason]`
|
19
|
+
- **Guaranteed results** - `call` always returns a result object, never raises
|
20
|
+
- **Fault vs Exception** - CMDx faults have special handling, other exceptions propagate in `call!`
|
21
|
+
|
22
|
+
## Exception Handling Behavior
|
23
|
+
|
24
|
+
### Non-bang Call (`call`)
|
25
|
+
|
26
|
+
The `call` method captures **all** unhandled exceptions and converts them to
|
27
|
+
failed results, ensuring that no exceptions escape the task execution boundary.
|
28
|
+
This provides consistent, predictable behavior for result processing.
|
7
29
|
|
8
30
|
```ruby
|
9
|
-
|
31
|
+
class ProcessUserOrderTask < CMDx::Task
|
32
|
+
|
33
|
+
def call
|
34
|
+
# This will raise a NoMethodError
|
35
|
+
undefined_method_call
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
result = ProcessUserOrderTask.call
|
10
41
|
result.state #=> "interrupted"
|
11
42
|
result.status #=> "failed"
|
43
|
+
result.failed? #=> true
|
12
44
|
result.metadata #=> {
|
13
|
-
#=> reason: "[
|
14
|
-
#=> original_exception: <
|
45
|
+
#=> reason: "[NoMethodError] undefined method `undefined_method_call`",
|
46
|
+
#=> original_exception: <NoMethodError>
|
15
47
|
#=> }
|
16
48
|
```
|
17
49
|
|
18
|
-
|
50
|
+
> [!NOTE]
|
51
|
+
> The `call` method ensures no exceptions escape task execution, making it ideal
|
52
|
+
> for workflow processing and scenarios where you need guaranteed result objects.
|
53
|
+
|
54
|
+
### Exception Metadata Structure
|
55
|
+
|
56
|
+
Captured exceptions populate result metadata with structured information:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class ConnectDatabaseTask < CMDx::Task
|
60
|
+
|
61
|
+
def call
|
62
|
+
# Simulate a database connection error
|
63
|
+
raise ActiveRecord::ConnectionNotEstablished, "Database unavailable"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
result = ConnectDatabaseTask.call
|
69
|
+
|
70
|
+
# Exception information in metadata
|
71
|
+
result.metadata[:reason] #=> "[ActiveRecord::ConnectionNotEstablished] Database unavailable"
|
72
|
+
result.metadata[:original_exception] #=> <ActiveRecord::ConnectionNotEstablished>
|
73
|
+
result.metadata[:original_exception].class #=> ActiveRecord::ConnectionNotEstablished
|
74
|
+
result.metadata[:original_exception].message #=> "Database unavailable"
|
75
|
+
result.metadata[:original_exception].backtrace #=> ["..."]
|
76
|
+
```
|
77
|
+
|
78
|
+
### Accessing Original Exception Details
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
result = ProcessUserOrderTask.call
|
82
|
+
|
83
|
+
if result.failed? && result.metadata[:original_exception]
|
84
|
+
original = result.metadata[:original_exception]
|
85
|
+
|
86
|
+
puts "Exception type: #{original.class}"
|
87
|
+
puts "Exception message: #{original.message}"
|
88
|
+
puts "Exception backtrace:"
|
89
|
+
puts original.backtrace.first(5).join("\n")
|
90
|
+
|
91
|
+
# Check exception type for specific handling
|
92
|
+
case original
|
93
|
+
when ActiveRecord::RecordNotFound
|
94
|
+
handle_missing_record(original)
|
95
|
+
when Net::TimeoutError
|
96
|
+
handle_timeout_error(original)
|
97
|
+
when StandardError
|
98
|
+
handle_generic_error(original)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
## Bang Call (`call!`)
|
104
|
+
|
105
|
+
The `call!` method allows unhandled exceptions to propagate **unless** they are
|
106
|
+
CMDx faults that match the `task_halt` configuration. This enables exception-based
|
107
|
+
control flow while still providing structured fault handling.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class ProcessUserOrderTask < CMDx::Task
|
111
|
+
|
112
|
+
def call
|
113
|
+
# This will raise a NoMethodError directly
|
114
|
+
undefined_method_call
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
ProcessUserOrderTask.call!
|
121
|
+
rescue NoMethodError => e
|
122
|
+
puts "Caught original exception: #{e.message}"
|
123
|
+
# Handle the original exception directly
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Fault vs Exception Behavior
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class ProcessOrderPaymentTask < CMDx::Task
|
131
|
+
|
132
|
+
def call
|
133
|
+
if context.simulate_fault
|
134
|
+
fail!(reason: "Controlled failure") # Becomes CMDx::Failed
|
135
|
+
else
|
136
|
+
raise StandardError, "Uncontrolled error" # Remains StandardError
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
# Fault behavior (controlled)
|
143
|
+
begin
|
144
|
+
ProcessOrderPaymentTask.call!(simulate_fault: true)
|
145
|
+
rescue CMDx::Failed => e
|
146
|
+
puts "Caught CMDx fault: #{e.message}"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Exception behavior (uncontrolled)
|
150
|
+
begin
|
151
|
+
ProcessOrderPaymentTask.call!(simulate_fault: false)
|
152
|
+
rescue StandardError => e
|
153
|
+
puts "Caught standard exception: #{e.message}"
|
154
|
+
end
|
155
|
+
```
|
19
156
|
|
20
|
-
|
157
|
+
## Exception Classification
|
158
|
+
|
159
|
+
### Protected Exceptions
|
160
|
+
|
161
|
+
Certain CMDx-specific exceptions are always allowed to propagate and are never
|
162
|
+
converted to failed results:
|
21
163
|
|
22
164
|
```ruby
|
23
|
-
|
165
|
+
class ProcessUndefinedOrderTask < CMDx::Task
|
166
|
+
# Intentionally not implementing call method
|
167
|
+
end
|
168
|
+
|
169
|
+
# These exceptions always propagate regardless of call method
|
170
|
+
begin
|
171
|
+
ProcessUndefinedOrderTask.call
|
172
|
+
rescue CMDx::UndefinedCallError => e
|
173
|
+
puts "This exception is never converted to a failed result"
|
174
|
+
end
|
175
|
+
|
176
|
+
begin
|
177
|
+
ProcessUndefinedOrderTask.call!
|
178
|
+
rescue CMDx::UndefinedCallError => e
|
179
|
+
puts "This exception propagates normally in call! too"
|
180
|
+
end
|
24
181
|
```
|
25
182
|
|
183
|
+
### CMDx Fault Handling
|
184
|
+
|
185
|
+
CMDx faults have special handling in both call methods:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class ProcessOrderWithHaltTask < CMDx::Task
|
189
|
+
# Configure to halt on failures
|
190
|
+
task_settings!(task_halt: [CMDx::Result::FAILED])
|
191
|
+
|
192
|
+
def call
|
193
|
+
fail!(reason: "This is a controlled failure")
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# With call - fault becomes failed result
|
198
|
+
result = ProcessOrderWithHaltTask.call
|
199
|
+
result.failed? #=> true
|
200
|
+
|
201
|
+
# With call! - fault becomes exception (due to task_halt configuration)
|
202
|
+
begin
|
203
|
+
ProcessOrderWithHaltTask.call!
|
204
|
+
rescue CMDx::Failed => e
|
205
|
+
puts "Fault converted to exception: #{e.message}"
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
> [!IMPORTANT]
|
210
|
+
> Always preserve original exception information in metadata when handling
|
211
|
+
> exceptions manually. This maintains debugging capabilities and error traceability.
|
212
|
+
|
26
213
|
---
|
27
214
|
|
28
|
-
- **Prev:** [Interruptions - Faults](
|
29
|
-
- **Next:** [Outcomes - Result](
|
215
|
+
- **Prev:** [Interruptions - Faults](faults.md)
|
216
|
+
- **Next:** [Outcomes - Result](../outcomes/result.md)
|
@@ -1,89 +1,241 @@
|
|
1
1
|
# Interruptions - Faults
|
2
2
|
|
3
|
-
Faults are the mechanisms by which
|
4
|
-
|
5
|
-
|
3
|
+
Faults are the exception mechanisms by which CMDx halts task execution via the
|
4
|
+
`skip!` and `fail!` methods. When tasks are executed with the bang `call!` method,
|
5
|
+
fault exceptions matching the task's interruption status are raised, enabling
|
6
|
+
sophisticated exception handling and control flow patterns.
|
6
7
|
|
7
|
-
##
|
8
|
+
## Table of Contents
|
8
9
|
|
9
|
-
|
10
|
+
- [TLDR](#tldr)
|
11
|
+
- [Fault Types](#fault-types)
|
12
|
+
- [Basic Exception Handling](#basic-exception-handling)
|
13
|
+
- [Fault Context Access](#fault-context-access)
|
14
|
+
- [Advanced Fault Matching](#advanced-fault-matching)
|
15
|
+
- [Fault Propagation (`throw!`)](#fault-propagation-throw)
|
16
|
+
- [Fault Chain Analysis](#fault-chain-analysis)
|
17
|
+
- [Task Halt Configuration](#task-halt-configuration)
|
18
|
+
|
19
|
+
## TLDR
|
20
|
+
|
21
|
+
- **Fault types** - `CMDx::Skipped` (from `skip!`) and `CMDx::Failed` (from `fail!`)
|
22
|
+
- **Exception handling** - Use `rescue CMDx::Fault` to catch both types
|
23
|
+
- **Full context** - Faults provide access to `result`, `task`, `context`, and `chain`
|
24
|
+
- **Advanced matching** - Use `for?(TaskClass)` and `matches? { |f| condition }` for specific fault handling
|
25
|
+
- **Propagation** - Use `throw!(result)` to bubble up failures while preserving fault context
|
26
|
+
|
27
|
+
## Fault Types
|
28
|
+
|
29
|
+
CMDx provides two primary fault types that inherit from the base `CMDx::Fault` class:
|
30
|
+
|
31
|
+
- **`CMDx::Skipped`** - Raised when a task is skipped via `skip!`
|
32
|
+
- **`CMDx::Failed`** - Raised when a task fails via `fail!`
|
33
|
+
|
34
|
+
Both fault types provide full access to the task execution context, including
|
35
|
+
the result object, task instance, context data, and chain information.
|
36
|
+
|
37
|
+
> [!NOTE]
|
38
|
+
> All fault exceptions (`CMDx::Skipped` and `CMDx::Failed`) inherit from the base `CMDx::Fault` class and provide access to the complete task execution context.
|
39
|
+
|
40
|
+
## Basic Exception Handling
|
41
|
+
|
42
|
+
Use standard Ruby `rescue` blocks to handle faults with custom logic:
|
10
43
|
|
11
44
|
```ruby
|
12
45
|
begin
|
13
|
-
|
14
|
-
rescue CMDx::Skipped
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
rescue CMDx::
|
19
|
-
#
|
46
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
47
|
+
rescue CMDx::Skipped => e
|
48
|
+
# Handle skipped tasks
|
49
|
+
logger.info "Task skipped: #{e.message}"
|
50
|
+
e.result.metadata[:reason] #=> "Order already processed"
|
51
|
+
rescue CMDx::Failed => e
|
52
|
+
# Handle failed tasks
|
53
|
+
logger.error "Task failed: #{e.message}"
|
54
|
+
e.result.metadata[:error_code] #=> "PAYMENT_DECLINED"
|
55
|
+
rescue CMDx::Fault => e
|
56
|
+
# Handle any fault (skipped or failed)
|
57
|
+
logger.warn "Task interrupted: #{e.message}"
|
20
58
|
end
|
21
59
|
```
|
22
60
|
|
23
|
-
##
|
61
|
+
## Fault Context Access
|
24
62
|
|
25
|
-
Faults
|
63
|
+
Faults provide comprehensive access to task execution context:
|
26
64
|
|
27
65
|
```ruby
|
28
66
|
begin
|
29
|
-
|
30
|
-
rescue CMDx::
|
31
|
-
#
|
67
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
68
|
+
rescue CMDx::Fault => e
|
69
|
+
# Result information
|
70
|
+
e.result.status #=> "failed" or "skipped"
|
71
|
+
e.result.metadata[:reason] #=> "Insufficient inventory"
|
72
|
+
e.result.runtime #=> 0.05
|
73
|
+
|
74
|
+
# Task information
|
75
|
+
e.task.class.name #=> "ProcessUserOrderTask"
|
76
|
+
e.task.id #=> "abc123..."
|
77
|
+
|
78
|
+
# Context data
|
79
|
+
e.context.order_id #=> 123
|
80
|
+
e.context.customer_email #=> "user@example.com"
|
81
|
+
|
82
|
+
# Chain information
|
83
|
+
e.chain.id #=> "def456..."
|
84
|
+
e.chain.results.size #=> 3
|
32
85
|
end
|
33
86
|
```
|
34
87
|
|
35
|
-
##
|
88
|
+
## Advanced Fault Matching
|
89
|
+
|
90
|
+
### Task-Specific Matching (`for?`)
|
36
91
|
|
37
|
-
|
92
|
+
Match faults only from specific task classes using the `for?` method:
|
38
93
|
|
39
94
|
```ruby
|
40
95
|
begin
|
41
|
-
|
42
|
-
rescue CMDx::
|
43
|
-
#
|
96
|
+
WorkflowProcessUserOrdersTask.call!(orders: orders)
|
97
|
+
rescue CMDx::Skipped.for?(ProcessUserOrderTask, ValidateUserOrderTask) => e
|
98
|
+
# Handle skips only from specific task types
|
99
|
+
logger.info "Order processing skipped: #{e.task.class.name}"
|
100
|
+
reschedule_order_processing(e.context.order_id)
|
101
|
+
rescue CMDx::Failed.for?(ProcessOrderPaymentTask, ProcessCardChargeTask) => e
|
102
|
+
# Handle failures only from payment-related tasks
|
103
|
+
logger.error "Payment processing failed: #{e.message}"
|
104
|
+
retry_with_backup_payment_method(e.context)
|
44
105
|
end
|
45
106
|
```
|
46
107
|
|
47
|
-
|
48
|
-
> All fault exceptions have access to the `for?` and `matches?` methods.
|
108
|
+
### Custom Matching Logic (`matches?`)
|
49
109
|
|
50
|
-
|
110
|
+
Use the `matches?` method with blocks for sophisticated fault matching:
|
51
111
|
|
52
|
-
|
53
|
-
|
112
|
+
```ruby
|
113
|
+
begin
|
114
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
115
|
+
rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_code] == "PAYMENT_DECLINED" } => e
|
116
|
+
# Handle specific payment errors
|
117
|
+
retry_with_different_payment_method(e.context)
|
118
|
+
rescue CMDx::Fault.matches? { |f| f.context.order_value > 1000 } => e
|
119
|
+
# Handle high-value order failures differently
|
120
|
+
escalate_to_manager(e)
|
121
|
+
rescue CMDx::Failed.matches? { |f| f.result.metadata[:reason]&.include?("timeout") } => e
|
122
|
+
# Handle timeout-specific failures
|
123
|
+
retry_with_longer_timeout(e)
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
> [!TIP]
|
128
|
+
> Use `for?` and `matches?` methods for advanced exception matching. The `for?` method is ideal for task-specific handling, while `matches?` enables custom logic-based fault filtering.
|
129
|
+
|
130
|
+
## Fault Propagation (`throw!`)
|
131
|
+
|
132
|
+
The `throw!` method enables fault propagation, allowing parent tasks to bubble up
|
133
|
+
failures from subtasks while preserving the original fault information:
|
134
|
+
|
135
|
+
### Basic Propagation
|
54
136
|
|
55
137
|
```ruby
|
56
|
-
class
|
138
|
+
class ProcessUserOrderTask < CMDx::Task
|
57
139
|
|
58
140
|
def call
|
59
|
-
|
141
|
+
# Execute subtask and propagate its failure
|
142
|
+
validation_result = ValidateUserOrderTask.call(context)
|
143
|
+
throw!(validation_result) if validation_result.failed?
|
144
|
+
|
145
|
+
payment_result = ProcessOrderPaymentTask.call(context)
|
146
|
+
throw!(payment_result) # failed or skipped
|
60
147
|
|
61
|
-
#
|
148
|
+
# Continue with main logic
|
149
|
+
finalize_order
|
62
150
|
end
|
63
151
|
|
64
152
|
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### Propagation with Additional Context
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class ProcessOrderWorkflowTask < CMDx::Task
|
159
|
+
|
160
|
+
def call
|
161
|
+
step1_result = ValidateOrderDataTask.call(context)
|
162
|
+
|
163
|
+
if step1_result.failed?
|
164
|
+
# Propagate with additional context
|
165
|
+
throw!(step1_result, {
|
166
|
+
workflow_stage: "initial_validation",
|
167
|
+
attempted_at: Time.now,
|
168
|
+
can_retry: true
|
169
|
+
})
|
170
|
+
end
|
171
|
+
|
172
|
+
continue_workflow
|
173
|
+
end
|
65
174
|
|
66
|
-
|
67
|
-
result.state #=> "interrupted"
|
68
|
-
result.status #=> "skipped"
|
69
|
-
result.metadata #=> { reason: "Order confirmation could not be sent due to invalid email." }
|
175
|
+
end
|
70
176
|
```
|
71
177
|
|
72
|
-
> [!
|
73
|
-
> `throw!`
|
74
|
-
|
178
|
+
> [!IMPORTANT]
|
179
|
+
> Use `throw!` to propagate failures while preserving the original fault context. This maintains the fault chain for debugging and provides better error traceability.
|
180
|
+
|
181
|
+
## Fault Chain Analysis
|
182
|
+
|
183
|
+
Results provide methods for analyzing fault propagation chains:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
result = ProcessOrderWorkflowTask.call(data: invalid_data)
|
187
|
+
|
188
|
+
if result.failed?
|
189
|
+
# Find the original cause of failure
|
190
|
+
original_failure = result.caused_failure
|
191
|
+
puts "Original failure: #{original_failure.task.class.name}"
|
192
|
+
puts "Reason: #{original_failure.metadata[:reason]}"
|
193
|
+
|
194
|
+
# Find what threw the failure to this result
|
195
|
+
throwing_task = result.threw_failure
|
196
|
+
puts "Failure thrown by: #{throwing_task.task.class.name}" if throwing_task
|
197
|
+
|
198
|
+
# Check if this result caused or threw the failure
|
199
|
+
if result.caused_failure?
|
200
|
+
puts "This task was the original cause"
|
201
|
+
elsif result.threw_failure?
|
202
|
+
puts "This task threw a failure from another task"
|
203
|
+
elsif result.thrown_failure?
|
204
|
+
puts "This task failed due to a thrown failure"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
```
|
75
208
|
|
76
|
-
##
|
209
|
+
## Task Halt Configuration
|
77
210
|
|
78
|
-
|
211
|
+
Control which statuses raise exceptions using the `task_halt` setting:
|
79
212
|
|
80
213
|
```ruby
|
81
|
-
|
82
|
-
|
83
|
-
|
214
|
+
class ProcessUserOrderTask < CMDx::Task
|
215
|
+
# Only failed tasks raise exceptions on call!
|
216
|
+
task_settings!(task_halt: [CMDx::Result::FAILED])
|
217
|
+
|
218
|
+
def call
|
219
|
+
skip!(reason: "Order already processed") if already_processed?
|
220
|
+
# This will NOT raise an exception on call!
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class ValidateUserDataTask < CMDx::Task
|
225
|
+
# Both failed and skipped tasks raise exceptions
|
226
|
+
task_settings!(task_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
|
227
|
+
|
228
|
+
def call
|
229
|
+
skip!(reason: "Validation not required") if skip_validation?
|
230
|
+
# This WILL raise an exception on call!
|
231
|
+
end
|
232
|
+
end
|
84
233
|
```
|
85
234
|
|
235
|
+
> [!WARNING]
|
236
|
+
> Task halt configuration only affects the `call!` method. The `call` method always captures all exceptions and converts them to result objects regardless of halt settings.
|
237
|
+
|
86
238
|
---
|
87
239
|
|
88
|
-
- **Prev:** [Interruptions - Halt](
|
89
|
-
- **Next:** [Interruptions - Exceptions](
|
240
|
+
- **Prev:** [Interruptions - Halt](halt.md)
|
241
|
+
- **Next:** [Interruptions - Exceptions](exceptions.md)
|