cmdx 1.12.0 → 1.14.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/CHANGELOG.md +88 -71
- data/LICENSE.txt +3 -20
- data/README.md +8 -7
- data/lib/cmdx/attribute.rb +21 -5
- data/lib/cmdx/chain.rb +18 -4
- data/lib/cmdx/context.rb +18 -0
- data/lib/cmdx/executor.rb +35 -30
- data/lib/cmdx/result.rb +45 -2
- data/lib/cmdx/task.rb +22 -1
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +67 -37
- metadata +3 -57
- data/.cursor/prompts/docs.md +0 -12
- data/.cursor/prompts/llms.md +0 -8
- data/.cursor/prompts/rspec.md +0 -24
- data/.cursor/prompts/yardoc.md +0 -15
- data/.cursor/rules/cursor-instructions.mdc +0 -68
- data/.irbrc +0 -18
- data/.rspec +0 -4
- data/.rubocop.yml +0 -95
- data/.ruby-version +0 -1
- data/.yard-lint.yml +0 -174
- data/.yardopts +0 -7
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +0 -1
- data/docs/attributes/coercions.md +0 -155
- data/docs/attributes/defaults.md +0 -77
- data/docs/attributes/definitions.md +0 -283
- data/docs/attributes/naming.md +0 -68
- data/docs/attributes/transformations.md +0 -63
- data/docs/attributes/validations.md +0 -336
- data/docs/basics/chain.md +0 -108
- data/docs/basics/context.md +0 -121
- data/docs/basics/execution.md +0 -96
- data/docs/basics/setup.md +0 -84
- data/docs/callbacks.md +0 -157
- data/docs/configuration.md +0 -314
- data/docs/deprecation.md +0 -145
- data/docs/getting_started.md +0 -126
- data/docs/index.md +0 -134
- data/docs/internationalization.md +0 -126
- data/docs/interruptions/exceptions.md +0 -52
- data/docs/interruptions/faults.md +0 -169
- data/docs/interruptions/halt.md +0 -216
- data/docs/logging.md +0 -94
- data/docs/middlewares.md +0 -191
- data/docs/outcomes/result.md +0 -194
- data/docs/outcomes/states.md +0 -66
- data/docs/outcomes/statuses.md +0 -65
- data/docs/retries.md +0 -121
- data/docs/stylesheets/extra.css +0 -42
- data/docs/tips_and_tricks.md +0 -157
- data/docs/workflows.md +0 -226
- data/examples/active_record_database_transaction.md +0 -27
- data/examples/active_record_query_tagging.md +0 -46
- data/examples/flipper_feature_flags.md +0 -50
- data/examples/paper_trail_whatdunnit.md +0 -39
- data/examples/redis_idempotency.md +0 -71
- data/examples/sentry_error_tracking.md +0 -46
- data/examples/sidekiq_async_execution.md +0 -29
- data/examples/stoplight_circuit_breaker.md +0 -36
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +0 -1
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +0 -1
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
# Interruptions - Faults
|
|
2
|
-
|
|
3
|
-
Faults are exceptions raised by `execute!` when tasks halt. They carry rich context about execution state, enabling sophisticated error handling patterns.
|
|
4
|
-
|
|
5
|
-
## Fault Types
|
|
6
|
-
|
|
7
|
-
| Type | Triggered By | Use Case |
|
|
8
|
-
|------|--------------|----------|
|
|
9
|
-
| `CMDx::Fault` | Base class | Catch-all for any interruption |
|
|
10
|
-
| `CMDx::SkipFault` | `skip!` method | Optional processing, early returns |
|
|
11
|
-
| `CMDx::FailFault` | `fail!` method | Validation errors, processing failures |
|
|
12
|
-
|
|
13
|
-
!!! warning "Important"
|
|
14
|
-
|
|
15
|
-
All faults inherit from `CMDx::Fault` and expose result, task, context, and chain data.
|
|
16
|
-
|
|
17
|
-
## Fault Handling
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
begin
|
|
21
|
-
ProcessTicket.execute!(ticket_id: 456)
|
|
22
|
-
rescue CMDx::SkipFault => e
|
|
23
|
-
logger.info "Ticket processing skipped: #{e.message}"
|
|
24
|
-
schedule_retry(e.context.ticket_id)
|
|
25
|
-
rescue CMDx::FailFault => e
|
|
26
|
-
logger.error "Ticket processing failed: #{e.message}"
|
|
27
|
-
notify_admin(e.context.assigned_agent, e.result.metadata[:error_code])
|
|
28
|
-
rescue CMDx::Fault => e
|
|
29
|
-
logger.warn "Ticket processing interrupted: #{e.message}"
|
|
30
|
-
rollback_changes
|
|
31
|
-
end
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Data Access
|
|
35
|
-
|
|
36
|
-
Access rich execution data from fault exceptions:
|
|
37
|
-
|
|
38
|
-
```ruby
|
|
39
|
-
begin
|
|
40
|
-
LicenseActivation.execute!(license_key: key, machine_id: machine)
|
|
41
|
-
rescue CMDx::Fault => e
|
|
42
|
-
# Result information
|
|
43
|
-
e.result.state #=> "interrupted"
|
|
44
|
-
e.result.status #=> "failed" or "skipped"
|
|
45
|
-
e.result.reason #=> "License key already activated"
|
|
46
|
-
|
|
47
|
-
# Task information
|
|
48
|
-
e.task.class #=> <LicenseActivation>
|
|
49
|
-
e.task.id #=> "abc123..."
|
|
50
|
-
|
|
51
|
-
# Context data
|
|
52
|
-
e.context.license_key #=> "ABC-123-DEF"
|
|
53
|
-
e.context.machine_id #=> "[FILTERED]"
|
|
54
|
-
|
|
55
|
-
# Chain information
|
|
56
|
-
e.chain.id #=> "def456..."
|
|
57
|
-
e.chain.size #=> 3
|
|
58
|
-
end
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Advanced Matching
|
|
62
|
-
|
|
63
|
-
### Task-Specific Matching
|
|
64
|
-
|
|
65
|
-
Handle faults only from specific tasks using `for?`:
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
begin
|
|
69
|
-
DocumentWorkflow.execute!(document_data: data)
|
|
70
|
-
rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e
|
|
71
|
-
# Handle only document-related failures
|
|
72
|
-
retry_with_alternate_parser(e.context)
|
|
73
|
-
rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e
|
|
74
|
-
# Handle security-related skips
|
|
75
|
-
quarantine_for_review(e.context.document_id)
|
|
76
|
-
end
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Custom Logic Matching
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
begin
|
|
83
|
-
ReportGenerator.execute!(report: report_data)
|
|
84
|
-
rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e
|
|
85
|
-
escalate_large_dataset_failure(e)
|
|
86
|
-
rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e
|
|
87
|
-
abandon_report_generation(e)
|
|
88
|
-
rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
|
|
89
|
-
increase_memory_and_retry(e)
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Fault Propagation
|
|
94
|
-
|
|
95
|
-
Propagate failures with `throw!` to preserve context and maintain the error chain:
|
|
96
|
-
|
|
97
|
-
### Basic Propagation
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
class ReportGenerator < CMDx::Task
|
|
101
|
-
def work
|
|
102
|
-
# Throw if skipped or failed
|
|
103
|
-
validation_result = DataValidator.execute(context)
|
|
104
|
-
throw!(validation_result)
|
|
105
|
-
|
|
106
|
-
# Only throw if skipped
|
|
107
|
-
check_permissions = CheckPermissions.execute(context)
|
|
108
|
-
throw!(check_permissions) if check_permissions.skipped?
|
|
109
|
-
|
|
110
|
-
# Only throw if failed
|
|
111
|
-
data_result = DataProcessor.execute(context)
|
|
112
|
-
throw!(data_result) if data_result.failed?
|
|
113
|
-
|
|
114
|
-
# Continue processing
|
|
115
|
-
generate_report
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Additional Metadata
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
class BatchProcessor < CMDx::Task
|
|
124
|
-
def work
|
|
125
|
-
step_result = FileValidation.execute(context)
|
|
126
|
-
|
|
127
|
-
if step_result.failed?
|
|
128
|
-
throw!(step_result, {
|
|
129
|
-
batch_stage: "validation",
|
|
130
|
-
can_retry: true,
|
|
131
|
-
next_step: "file_repair"
|
|
132
|
-
})
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
continue_batch
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Chain Analysis
|
|
141
|
-
|
|
142
|
-
Trace fault origins and propagation through the execution chain:
|
|
143
|
-
|
|
144
|
-
```ruby
|
|
145
|
-
result = DocumentWorkflow.execute(invalid_data)
|
|
146
|
-
|
|
147
|
-
if result.failed?
|
|
148
|
-
# Trace the original failure
|
|
149
|
-
original = result.caused_failure
|
|
150
|
-
if original
|
|
151
|
-
puts "Original failure: #{original.task.class.name}"
|
|
152
|
-
puts "Reason: #{original.reason}"
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Find what propagated the failure
|
|
156
|
-
thrower = result.threw_failure
|
|
157
|
-
puts "Propagated by: #{thrower.task.class.name}" if thrower
|
|
158
|
-
|
|
159
|
-
# Analyze failure type
|
|
160
|
-
case
|
|
161
|
-
when result.caused_failure?
|
|
162
|
-
puts "This task was the original source"
|
|
163
|
-
when result.threw_failure?
|
|
164
|
-
puts "This task propagated a failure"
|
|
165
|
-
when result.thrown_failure?
|
|
166
|
-
puts "This task failed due to propagation"
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
```
|
data/docs/interruptions/halt.md
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
# Interruptions - Halt
|
|
2
|
-
|
|
3
|
-
Stop task execution intentionally using `skip!` or `fail!`. Both methods signal clear intent about why execution stopped.
|
|
4
|
-
|
|
5
|
-
## Skipping
|
|
6
|
-
|
|
7
|
-
Use `skip!` when the task doesn't need to run. It's a no-op, not an error.
|
|
8
|
-
|
|
9
|
-
!!! warning "Important"
|
|
10
|
-
|
|
11
|
-
Skipped tasks are considered "good" outcomes—they succeeded by doing nothing.
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
class ProcessInventory < CMDx::Task
|
|
15
|
-
def work
|
|
16
|
-
# Without a reason
|
|
17
|
-
skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
|
|
18
|
-
|
|
19
|
-
# With a reason
|
|
20
|
-
skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
|
|
21
|
-
|
|
22
|
-
inventory = Inventory.find(context.inventory_id)
|
|
23
|
-
|
|
24
|
-
if inventory.already_counted?
|
|
25
|
-
skip!("Inventory already counted today")
|
|
26
|
-
else
|
|
27
|
-
inventory.count!
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
result = ProcessInventory.execute(inventory_id: 456)
|
|
33
|
-
|
|
34
|
-
# Executed
|
|
35
|
-
result.status #=> "skipped"
|
|
36
|
-
|
|
37
|
-
# Without a reason
|
|
38
|
-
result.reason #=> "Unspecified"
|
|
39
|
-
|
|
40
|
-
# With a reason
|
|
41
|
-
result.reason #=> "Warehouse closed"
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Failing
|
|
45
|
-
|
|
46
|
-
Use `fail!` when the task can't complete successfully. It signals controlled, intentional failure:
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
class ProcessRefund < CMDx::Task
|
|
50
|
-
def work
|
|
51
|
-
# Without a reason
|
|
52
|
-
fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
|
|
53
|
-
|
|
54
|
-
refund = Refund.find(context.refund_id)
|
|
55
|
-
|
|
56
|
-
# With a reason
|
|
57
|
-
if refund.expired?
|
|
58
|
-
fail!("Refund period has expired")
|
|
59
|
-
elsif !refund.amount.positive?
|
|
60
|
-
fail!("Refund amount must be positive")
|
|
61
|
-
else
|
|
62
|
-
refund.process!
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
result = ProcessRefund.execute(refund_id: 789)
|
|
68
|
-
|
|
69
|
-
# Executed
|
|
70
|
-
result.status #=> "failed"
|
|
71
|
-
|
|
72
|
-
# Without a reason
|
|
73
|
-
result.reason #=> "Unspecified"
|
|
74
|
-
|
|
75
|
-
# With a reason
|
|
76
|
-
result.reason #=> "Refund period has expired"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Metadata Enrichment
|
|
80
|
-
|
|
81
|
-
Enrich halt calls with metadata for better debugging and error handling:
|
|
82
|
-
|
|
83
|
-
```ruby
|
|
84
|
-
class ProcessRenewal < CMDx::Task
|
|
85
|
-
def work
|
|
86
|
-
license = License.find(context.license_id)
|
|
87
|
-
|
|
88
|
-
if license.already_renewed?
|
|
89
|
-
# Without metadata
|
|
90
|
-
skip!("License already renewed")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
unless license.renewal_eligible?
|
|
94
|
-
# With metadata
|
|
95
|
-
fail!(
|
|
96
|
-
"License not eligible for renewal",
|
|
97
|
-
error_code: "LICENSE.NOT_ELIGIBLE",
|
|
98
|
-
retry_after: Time.current + 30.days
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
process_renewal
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
result = ProcessRenewal.execute(license_id: 567)
|
|
107
|
-
|
|
108
|
-
# Without metadata
|
|
109
|
-
result.metadata #=> {}
|
|
110
|
-
|
|
111
|
-
# With metadata
|
|
112
|
-
result.metadata #=> {
|
|
113
|
-
# error_code: "LICENSE.NOT_ELIGIBLE",
|
|
114
|
-
# retry_after: <Time 30 days from now>
|
|
115
|
-
# }
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## State Transitions
|
|
119
|
-
|
|
120
|
-
Halt methods trigger specific state and status transitions:
|
|
121
|
-
|
|
122
|
-
| Method | State | Status | Outcome |
|
|
123
|
-
|--------|-------|--------|---------|
|
|
124
|
-
| `skip!` | `interrupted` | `skipped` | `good? = true`, `bad? = true` |
|
|
125
|
-
| `fail!` | `interrupted` | `failed` | `good? = false`, `bad? = true` |
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
result = ProcessRenewal.execute(license_id: 567)
|
|
129
|
-
|
|
130
|
-
# State information
|
|
131
|
-
result.state #=> "interrupted"
|
|
132
|
-
result.status #=> "skipped" or "failed"
|
|
133
|
-
result.interrupted? #=> true
|
|
134
|
-
result.complete? #=> false
|
|
135
|
-
|
|
136
|
-
# Outcome categorization
|
|
137
|
-
result.good? #=> true for skipped, false for failed
|
|
138
|
-
result.bad? #=> true for both skipped and failed
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Execution Behavior
|
|
142
|
-
|
|
143
|
-
Halt methods behave differently depending on the call method used:
|
|
144
|
-
|
|
145
|
-
### Non-bang execution
|
|
146
|
-
|
|
147
|
-
Returns result object without raising exceptions:
|
|
148
|
-
|
|
149
|
-
```ruby
|
|
150
|
-
result = ProcessRefund.execute(refund_id: 789)
|
|
151
|
-
|
|
152
|
-
case result.status
|
|
153
|
-
when "success"
|
|
154
|
-
puts "Refund processed: $#{result.context.refund.amount}"
|
|
155
|
-
when "skipped"
|
|
156
|
-
puts "Refund skipped: #{result.reason}"
|
|
157
|
-
when "failed"
|
|
158
|
-
puts "Refund failed: #{result.reason}"
|
|
159
|
-
handle_refund_error(result.metadata[:error_code])
|
|
160
|
-
end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### Bang execution
|
|
164
|
-
|
|
165
|
-
Raises exceptions for halt conditions based on `task_breakpoints` configuration:
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
begin
|
|
169
|
-
result = ProcessRefund.execute!(refund_id: 789)
|
|
170
|
-
puts "Success: Refund processed"
|
|
171
|
-
rescue CMDx::SkipFault => e
|
|
172
|
-
puts "Skipped: #{e.message}"
|
|
173
|
-
rescue CMDx::FailFault => e
|
|
174
|
-
puts "Failed: #{e.message}"
|
|
175
|
-
handle_refund_failure(e.result.metadata[:error_code])
|
|
176
|
-
end
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Best Practices
|
|
180
|
-
|
|
181
|
-
Always provide a reason for better debugging and clearer exception messages:
|
|
182
|
-
|
|
183
|
-
```ruby
|
|
184
|
-
# Good: Clear, specific reason
|
|
185
|
-
skip!("Document processing paused for compliance review")
|
|
186
|
-
fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
|
|
187
|
-
|
|
188
|
-
# Acceptable: Generic, non-specific reason
|
|
189
|
-
skip!("Paused")
|
|
190
|
-
fail!("Unsupported")
|
|
191
|
-
|
|
192
|
-
# Bad: Default, cannot determine reason
|
|
193
|
-
skip! #=> "Unspecified"
|
|
194
|
-
fail! #=> "Unspecified"
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Manual Errors
|
|
198
|
-
|
|
199
|
-
For rare cases, manually add errors before halting:
|
|
200
|
-
|
|
201
|
-
!!! warning "Important"
|
|
202
|
-
|
|
203
|
-
Manual errors don't stop execution—you still need to call `fail!` or `skip!`.
|
|
204
|
-
|
|
205
|
-
```ruby
|
|
206
|
-
class ProcessRenewal < CMDx::Task
|
|
207
|
-
def work
|
|
208
|
-
if document.nonrenewable?
|
|
209
|
-
errors.add(:document, "not renewable")
|
|
210
|
-
fail!("document could not be renewed")
|
|
211
|
-
else
|
|
212
|
-
document.renew!
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
```
|
data/docs/logging.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Logging
|
|
2
|
-
|
|
3
|
-
CMDx automatically logs every task execution with structured data, making debugging and monitoring effortless. Choose from multiple formatters to match your logging infrastructure.
|
|
4
|
-
|
|
5
|
-
## Formatters
|
|
6
|
-
|
|
7
|
-
Choose the format that works best for your logging system:
|
|
8
|
-
|
|
9
|
-
| Formatter | Use Case | Output Style |
|
|
10
|
-
|-----------|----------|--------------|
|
|
11
|
-
| `Line` | Traditional logging | Single-line format |
|
|
12
|
-
| `Json` | Structured systems | Compact JSON |
|
|
13
|
-
| `KeyValue` | Log parsing | `key=value` pairs |
|
|
14
|
-
| `Logstash` | ELK stack | JSON with @version/@timestamp |
|
|
15
|
-
| `Raw` | Minimal output | Message content only |
|
|
16
|
-
|
|
17
|
-
Sample output:
|
|
18
|
-
|
|
19
|
-
```log
|
|
20
|
-
<!-- Success (INFO level) -->
|
|
21
|
-
I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
|
|
22
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
|
23
|
-
|
|
24
|
-
<!-- Skipped (WARN level) -->
|
|
25
|
-
W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
|
|
26
|
-
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
|
27
|
-
|
|
28
|
-
<!-- Failed (ERROR level) -->
|
|
29
|
-
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
|
|
30
|
-
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
|
31
|
-
|
|
32
|
-
<!-- Failed Chain -->
|
|
33
|
-
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
|
|
34
|
-
index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
!!! tip
|
|
38
|
-
|
|
39
|
-
Use logging as a low-level event stream to track all tasks in a request. Combine with correlation for powerful distributed tracing.
|
|
40
|
-
|
|
41
|
-
## Structure
|
|
42
|
-
|
|
43
|
-
Every log entry includes rich metadata. Available fields depend on execution context and outcome.
|
|
44
|
-
|
|
45
|
-
### Core Fields
|
|
46
|
-
|
|
47
|
-
| Field | Description | Example |
|
|
48
|
-
|-------|-------------|---------|
|
|
49
|
-
| `severity` | Log level | `INFO`, `WARN`, `ERROR` |
|
|
50
|
-
| `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
|
|
51
|
-
| `pid` | Process ID | `3784` |
|
|
52
|
-
|
|
53
|
-
### Task Information
|
|
54
|
-
|
|
55
|
-
| Field | Description | Example |
|
|
56
|
-
|-------|-------------|---------|
|
|
57
|
-
| `index` | Execution sequence position | `0`, `1`, `2` |
|
|
58
|
-
| `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
|
|
59
|
-
| `type` | Execution unit type | `Task`, `Workflow` |
|
|
60
|
-
| `class` | Task class name | `GenerateInvoiceTask` |
|
|
61
|
-
| `id` | Unique task instance ID | `018c2b95-b764-7615...` |
|
|
62
|
-
| `tags` | Custom categorization | `["billing", "financial"]` |
|
|
63
|
-
|
|
64
|
-
### Execution Data
|
|
65
|
-
|
|
66
|
-
| Field | Description | Example |
|
|
67
|
-
|-------|-------------|---------|
|
|
68
|
-
| `state` | Lifecycle state | `complete`, `interrupted` |
|
|
69
|
-
| `status` | Business outcome | `success`, `skipped`, `failed` |
|
|
70
|
-
| `outcome` | Final classification | `success`, `interrupted` |
|
|
71
|
-
| `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
|
|
72
|
-
|
|
73
|
-
### Failure Chain
|
|
74
|
-
|
|
75
|
-
| Field | Description |
|
|
76
|
-
|-------|-------------|
|
|
77
|
-
| `reason` | Reason given for the stoppage |
|
|
78
|
-
| `caused` | Cause exception details |
|
|
79
|
-
| `caused_failure` | Original failing task details |
|
|
80
|
-
| `threw_failure` | Task that propagated the failure |
|
|
81
|
-
|
|
82
|
-
## Usage
|
|
83
|
-
|
|
84
|
-
Access the framework logger directly within tasks:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
class ProcessSubscription < CMDx::Task
|
|
88
|
-
def work
|
|
89
|
-
logger.debug { "Activated feature flags: #{Features.active_flags}" }
|
|
90
|
-
# Your logic here...
|
|
91
|
-
logger.info("Subscription processed")
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
```
|
data/docs/middlewares.md
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# Middlewares
|
|
2
|
-
|
|
3
|
-
Wrap task execution with middleware for cross-cutting concerns like authentication, caching, timeouts, and monitoring. Think Rack middleware, but for your business logic.
|
|
4
|
-
|
|
5
|
-
See [Global Configuration](getting_started.md#middlewares) for framework-wide setup.
|
|
6
|
-
|
|
7
|
-
## Execution Order
|
|
8
|
-
|
|
9
|
-
Middleware wraps task execution in layers, like an onion:
|
|
10
|
-
|
|
11
|
-
!!! note
|
|
12
|
-
|
|
13
|
-
First registered = outermost wrapper. They execute in registration order.
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
class ProcessCampaign < CMDx::Task
|
|
17
|
-
register :middleware, AuditMiddleware # 1st: outermost wrapper
|
|
18
|
-
register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
|
|
19
|
-
register :middleware, CacheMiddleware # 3rd: innermost wrapper
|
|
20
|
-
|
|
21
|
-
def work
|
|
22
|
-
# Your logic here...
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Execution flow:
|
|
27
|
-
# 1. AuditMiddleware (before)
|
|
28
|
-
# 2. AuthorizationMiddleware (before)
|
|
29
|
-
# 3. CacheMiddleware (before)
|
|
30
|
-
# 4. [task execution]
|
|
31
|
-
# 5. CacheMiddleware (after)
|
|
32
|
-
# 6. AuthorizationMiddleware (after)
|
|
33
|
-
# 7. AuditMiddleware (after)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Declarations
|
|
37
|
-
|
|
38
|
-
### Proc or Lambda
|
|
39
|
-
|
|
40
|
-
Use anonymous functions for simple middleware logic:
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
class ProcessCampaign < CMDx::Task
|
|
44
|
-
# Proc
|
|
45
|
-
register :middleware, proc do |task, options, &block|
|
|
46
|
-
result = block.call
|
|
47
|
-
Analytics.track(result.status)
|
|
48
|
-
result
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Lambda
|
|
52
|
-
register :middleware, ->(task, options, &block) {
|
|
53
|
-
result = block.call
|
|
54
|
-
Analytics.track(result.status)
|
|
55
|
-
result
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Class or Module
|
|
61
|
-
|
|
62
|
-
For complex middleware logic, use classes or modules:
|
|
63
|
-
|
|
64
|
-
```ruby
|
|
65
|
-
class TelemetryMiddleware
|
|
66
|
-
def call(task, options)
|
|
67
|
-
result = yield
|
|
68
|
-
Telemetry.record(result.status)
|
|
69
|
-
ensure
|
|
70
|
-
result # Always return result
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
class ProcessCampaign < CMDx::Task
|
|
75
|
-
# Class or Module
|
|
76
|
-
register :middleware, TelemetryMiddleware
|
|
77
|
-
|
|
78
|
-
# Instance
|
|
79
|
-
register :middleware, TelemetryMiddleware.new
|
|
80
|
-
|
|
81
|
-
# With options
|
|
82
|
-
register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
|
|
83
|
-
register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
|
|
84
|
-
end
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Removals
|
|
88
|
-
|
|
89
|
-
Remove class or module-based middleware globally or per-task:
|
|
90
|
-
|
|
91
|
-
!!! warning
|
|
92
|
-
|
|
93
|
-
Each `deregister` call removes one middleware. Use multiple calls for batch removals.
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
class ProcessCampaign < CMDx::Task
|
|
97
|
-
# Class or Module (no instances)
|
|
98
|
-
deregister :middleware, TelemetryMiddleware
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Built-in
|
|
103
|
-
|
|
104
|
-
### Timeout
|
|
105
|
-
|
|
106
|
-
Prevent tasks from running too long:
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
class ProcessReport < CMDx::Task
|
|
110
|
-
# Default timeout: 3 seconds
|
|
111
|
-
register :middleware, CMDx::Middlewares::Timeout
|
|
112
|
-
|
|
113
|
-
# Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
|
|
114
|
-
register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
|
|
115
|
-
|
|
116
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
117
|
-
register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
|
|
118
|
-
|
|
119
|
-
def work
|
|
120
|
-
# Your logic here...
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def max_processing_time
|
|
126
|
-
Rails.env.production? ? 2 : 10
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Slow task
|
|
131
|
-
result = ProcessReport.execute
|
|
132
|
-
|
|
133
|
-
result.state #=> "interrupted"
|
|
134
|
-
result.status #=> "failure"
|
|
135
|
-
result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
|
|
136
|
-
result.cause #=> <CMDx::TimeoutError>
|
|
137
|
-
result.metadata #=> { limit: 3 }
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Correlate
|
|
141
|
-
|
|
142
|
-
Add correlation IDs for distributed tracing and request tracking:
|
|
143
|
-
|
|
144
|
-
```ruby
|
|
145
|
-
class ProcessExport < CMDx::Task
|
|
146
|
-
# Default correlation ID generation
|
|
147
|
-
register :middleware, CMDx::Middlewares::Correlate
|
|
148
|
-
|
|
149
|
-
# Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
|
|
150
|
-
register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
|
|
151
|
-
|
|
152
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
153
|
-
register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
|
|
154
|
-
|
|
155
|
-
def work
|
|
156
|
-
# Your logic here...
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
private
|
|
160
|
-
|
|
161
|
-
def correlation_enabled?
|
|
162
|
-
ENV["CORRELATION_ENABLED"] == "true"
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
result = ProcessExport.execute
|
|
167
|
-
result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Runtime
|
|
171
|
-
|
|
172
|
-
Track task execution time in milliseconds using a monotonic clock:
|
|
173
|
-
|
|
174
|
-
```ruby
|
|
175
|
-
class PerformanceMonitoringCheck
|
|
176
|
-
def call(task)
|
|
177
|
-
task.context.tenant.monitoring_enabled?
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
class ProcessExport < CMDx::Task
|
|
182
|
-
# Default timeout is 3 seconds
|
|
183
|
-
register :middleware, CMDx::Middlewares::Runtime
|
|
184
|
-
|
|
185
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
186
|
-
register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
result = ProcessExport.execute
|
|
190
|
-
result.metadata #=> { runtime: 1247 } (ms)
|
|
191
|
-
```
|