cmdx 1.13.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 +84 -76
- data/LICENSE.txt +3 -20
- data/README.md +8 -7
- data/lib/cmdx/attribute.rb +21 -5
- data/lib/cmdx/context.rb +16 -0
- data/lib/cmdx/executor.rb +9 -9
- data/lib/cmdx/result.rb +27 -7
- data/lib/cmdx/task.rb +19 -0
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +62 -36
- 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 -152
- data/docs/basics/setup.md +0 -107
- data/docs/callbacks.md +0 -157
- data/docs/configuration.md +0 -314
- data/docs/deprecation.md +0 -143
- data/docs/getting_started.md +0 -137
- 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 -90
- data/docs/middlewares.md +0 -191
- data/docs/outcomes/result.md +0 -197
- 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
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,90 +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, [2025-12-23T17:04:07.292614Z #20108] INFO -- cmdx: {index: 1, chain_id: "019b4c2b-087b-79be-8ef2-96c11b659df5", type: "Task", tags: [], class: "GenerateInvoice", dry_run: false, id: "019b4c2b-0878-704d-ba0b-daa5410123ec", state: "complete", status: "success", outcome: "success", metadata: {runtime: 187}}
|
|
22
|
-
|
|
23
|
-
<!-- Skipped (INFO level) -->
|
|
24
|
-
I, [2025-12-23T17:04:11.496881Z #20139] INFO -- cmdx: {index: 2, chain_id: "019b4c2b-18e8-7af6-a38b-63b042c4fbed", type: "Task", tags: [], class: "ValidateCustomer", dry_run: false, id: "019b4c2b-18e5-7230-af7e-5b4a4bd7cda2", state: "interrupted", status: "skipped", outcome: "skipped", metadata: {}, reason: "Customer already validated", cause: #<CMDx::SkipFault: Customer already validated>, rolled_back: false}
|
|
25
|
-
|
|
26
|
-
<!-- Failed (INFO level) -->
|
|
27
|
-
I, [2025-12-23T17:04:15.875306Z #20173] INFO -- cmdx: {index: 3, chain_id: "019b4c2b-2a02-7dbc-b713-b20a7379704f", type: "Task", tags: [], class: "CalculateTax", dry_run: false, id: "019b4c2b-2a00-70b7-9fab-2f14db9139ef", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}
|
|
28
|
-
|
|
29
|
-
<!-- Failed Chain -->
|
|
30
|
-
I, [2025-12-23T17:04:20.972539Z #20209] INFO -- cmdx: {index: 0, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Workflow", tags: [], class: "BillingWorkflow", dry_run: false, id: "019b4c2b-3de6-70b9-9c16-5be13b1a463c", state: "interrupted", status: "failed", outcome: "interrupted", metadata: {}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false, threw_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}, caused_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
!!! tip
|
|
34
|
-
|
|
35
|
-
Use logging as a low-level event stream to track all tasks in a request. Combine with correlation for powerful distributed tracing.
|
|
36
|
-
|
|
37
|
-
## Structure
|
|
38
|
-
|
|
39
|
-
Every log entry includes rich metadata. Available fields depend on execution context and outcome.
|
|
40
|
-
|
|
41
|
-
### Core Fields
|
|
42
|
-
|
|
43
|
-
| Field | Description | Example |
|
|
44
|
-
|-------|-------------|---------|
|
|
45
|
-
| `severity` | Log level | `INFO`, `WARN`, `ERROR` |
|
|
46
|
-
| `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
|
|
47
|
-
| `pid` | Process ID | `3784` |
|
|
48
|
-
|
|
49
|
-
### Task Information
|
|
50
|
-
|
|
51
|
-
| Field | Description | Example |
|
|
52
|
-
|-------|-------------|---------|
|
|
53
|
-
| `index` | Execution sequence position | `0`, `1`, `2` |
|
|
54
|
-
| `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
|
|
55
|
-
| `type` | Execution unit type | `Task`, `Workflow` |
|
|
56
|
-
| `class` | Task class name | `GenerateInvoiceTask` |
|
|
57
|
-
| `id` | Unique task instance ID | `018c2b95-b764-7615...` |
|
|
58
|
-
| `tags` | Custom categorization | `["billing", "financial"]` |
|
|
59
|
-
|
|
60
|
-
### Execution Data
|
|
61
|
-
|
|
62
|
-
| Field | Description | Example |
|
|
63
|
-
|-------|-------------|---------|
|
|
64
|
-
| `state` | Lifecycle state | `complete`, `interrupted` |
|
|
65
|
-
| `status` | Business outcome | `success`, `skipped`, `failed` |
|
|
66
|
-
| `outcome` | Final classification | `success`, `interrupted` |
|
|
67
|
-
| `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
|
|
68
|
-
|
|
69
|
-
### Failure Chain
|
|
70
|
-
|
|
71
|
-
| Field | Description |
|
|
72
|
-
|-------|-------------|
|
|
73
|
-
| `reason` | Reason given for the stoppage |
|
|
74
|
-
| `caused` | Cause exception details |
|
|
75
|
-
| `caused_failure` | Original failing task details |
|
|
76
|
-
| `threw_failure` | Task that propagated the failure |
|
|
77
|
-
|
|
78
|
-
## Usage
|
|
79
|
-
|
|
80
|
-
Access the framework logger directly within tasks:
|
|
81
|
-
|
|
82
|
-
```ruby
|
|
83
|
-
class ProcessSubscription < CMDx::Task
|
|
84
|
-
def work
|
|
85
|
-
logger.debug { "Activated feature flags: #{Features.active_flags}" }
|
|
86
|
-
# Your logic here...
|
|
87
|
-
logger.info("Subscription processed")
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
```
|
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
|
-
```
|
data/docs/outcomes/result.md
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# Outcomes - Result
|
|
2
|
-
|
|
3
|
-
Results are your window into task execution. They expose everything: outcome, state, timing, context, and metadata.
|
|
4
|
-
|
|
5
|
-
## Result Attributes
|
|
6
|
-
|
|
7
|
-
Access essential execution information:
|
|
8
|
-
|
|
9
|
-
!!! warning "Important"
|
|
10
|
-
|
|
11
|
-
Results are immutable after execution completes.
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
15
|
-
|
|
16
|
-
# Object data
|
|
17
|
-
result.task #=> <BuildApplication>
|
|
18
|
-
result.context #=> <CMDx::Context>
|
|
19
|
-
result.chain #=> <CMDx::Chain>
|
|
20
|
-
|
|
21
|
-
# Execution data
|
|
22
|
-
result.state #=> "interrupted"
|
|
23
|
-
result.status #=> "failed"
|
|
24
|
-
|
|
25
|
-
# Fault data
|
|
26
|
-
result.reason #=> "Build tool not found"
|
|
27
|
-
result.cause #=> <CMDx::FailFault>
|
|
28
|
-
result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Lifecycle Information
|
|
32
|
-
|
|
33
|
-
Check execution state, status, and rollback with predicate methods:
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
37
|
-
|
|
38
|
-
# State predicates (execution lifecycle)
|
|
39
|
-
result.complete? #=> true (successful completion)
|
|
40
|
-
result.interrupted? #=> false (no interruption)
|
|
41
|
-
result.executed? #=> true (execution finished)
|
|
42
|
-
|
|
43
|
-
# Status predicates (execution outcome)
|
|
44
|
-
result.success? #=> true (successful execution)
|
|
45
|
-
result.failed? #=> false (no failure)
|
|
46
|
-
result.skipped? #=> false (not skipped)
|
|
47
|
-
|
|
48
|
-
# Outcome categorization
|
|
49
|
-
result.good? #=> true (success or skipped)
|
|
50
|
-
result.bad? #=> false (skipped or failed)
|
|
51
|
-
|
|
52
|
-
# Rollback Status
|
|
53
|
-
result.rolled_back? #=> true (execution was rolled back)
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Outcome Analysis
|
|
57
|
-
|
|
58
|
-
Get a unified outcome string combining state and status:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
62
|
-
|
|
63
|
-
result.outcome #=> "success" (state and status)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Chain Analysis
|
|
67
|
-
|
|
68
|
-
Trace fault origins and propagation:
|
|
69
|
-
|
|
70
|
-
```ruby
|
|
71
|
-
result = DeploymentWorkflow.execute(app_name: "webapp")
|
|
72
|
-
|
|
73
|
-
if result.failed?
|
|
74
|
-
# Find the original cause of failure
|
|
75
|
-
if original_failure = result.caused_failure
|
|
76
|
-
puts "Root cause: #{original_failure.task.class.name}"
|
|
77
|
-
puts "Reason: #{original_failure.reason}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Find what threw the failure to this result
|
|
81
|
-
if throwing_task = result.threw_failure
|
|
82
|
-
puts "Failure source: #{throwing_task.task.class.name}"
|
|
83
|
-
puts "Reason: #{throwing_task.reason}"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Failure classification
|
|
87
|
-
result.caused_failure? #=> true if this result was the original cause
|
|
88
|
-
result.threw_failure? #=> true if this result threw a failure
|
|
89
|
-
result.thrown_failure? #=> true if this result received a thrown failure
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Index and Position
|
|
94
|
-
|
|
95
|
-
Results track their position within execution chains:
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
99
|
-
|
|
100
|
-
# Position in execution sequence
|
|
101
|
-
result.index #=> 0 (first task in chain)
|
|
102
|
-
|
|
103
|
-
# Access via chain
|
|
104
|
-
result.chain.results[result.index] == result #=> true
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Block Yield
|
|
108
|
-
|
|
109
|
-
Execute code with direct result access:
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
BuildApplication.execute(version: "1.2.3") do |result|
|
|
113
|
-
if result.success?
|
|
114
|
-
notify_deployment_ready(result)
|
|
115
|
-
elsif result.failed?
|
|
116
|
-
handle_build_failure(result)
|
|
117
|
-
else
|
|
118
|
-
log_skip_reason(result)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Handlers
|
|
124
|
-
|
|
125
|
-
Handle outcomes with functional-style methods. Handlers return the result for chaining:
|
|
126
|
-
|
|
127
|
-
```ruby
|
|
128
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
129
|
-
|
|
130
|
-
# Status-based handlers
|
|
131
|
-
result
|
|
132
|
-
.on(:success) { |result| notify_deployment_ready(result) }
|
|
133
|
-
.on(:failed) { |result| handle_build_failure(result) }
|
|
134
|
-
.on(:skipped) { |result| log_skip_reason(result) }
|
|
135
|
-
|
|
136
|
-
# State-based handlers
|
|
137
|
-
result
|
|
138
|
-
.on(:complete) { |result| update_build_status(result) }
|
|
139
|
-
.on(:interrupted) { |result| cleanup_partial_artifacts(result) }
|
|
140
|
-
.on(:executed) { |result| alert_operations_team(result) } #=> .on(:complete, :interrupted)
|
|
141
|
-
|
|
142
|
-
# Outcome-based handlers
|
|
143
|
-
result
|
|
144
|
-
.on(:good) { |result| increment_success_counter(result) } #=> .on(:success, :skipped)
|
|
145
|
-
.on(:bad) { |result| alert_operations_team(result) } #=> .on(:failed, :skipped)
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Pattern Matching
|
|
149
|
-
|
|
150
|
-
Use Ruby 3.0+ pattern matching for elegant outcome handling:
|
|
151
|
-
|
|
152
|
-
!!! warning "Important"
|
|
153
|
-
|
|
154
|
-
Pattern matching works with both array and hash deconstruction.
|
|
155
|
-
|
|
156
|
-
### Array Pattern
|
|
157
|
-
|
|
158
|
-
```ruby
|
|
159
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
160
|
-
|
|
161
|
-
case result
|
|
162
|
-
in ["complete", "success"]
|
|
163
|
-
redirect_to build_success_page
|
|
164
|
-
in ["interrupted", "failed"]
|
|
165
|
-
retry_build_with_backoff(result)
|
|
166
|
-
in ["interrupted", "skipped"]
|
|
167
|
-
log_skip_and_continue
|
|
168
|
-
end
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Hash Pattern
|
|
172
|
-
|
|
173
|
-
```ruby
|
|
174
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
175
|
-
|
|
176
|
-
case result
|
|
177
|
-
in { state: "complete", status: "success" }
|
|
178
|
-
celebrate_build_success
|
|
179
|
-
in { status: "failed", metadata: { retryable: true } }
|
|
180
|
-
schedule_build_retry(result)
|
|
181
|
-
in { bad: true, metadata: { reason: String => reason } }
|
|
182
|
-
escalate_build_error("Build failed: #{reason}")
|
|
183
|
-
end
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Pattern Guards
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
case result
|
|
190
|
-
in { status: "failed", metadata: { attempts: n } } if n < 3
|
|
191
|
-
retry_build_with_delay(result, n * 2)
|
|
192
|
-
in { status: "failed", metadata: { attempts: n } } if n >= 3
|
|
193
|
-
mark_build_permanently_failed(result)
|
|
194
|
-
in { runtime: time } if time > performance_threshold
|
|
195
|
-
investigate_build_performance(result)
|
|
196
|
-
end
|
|
197
|
-
```
|