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/getting_started.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Getting Started
|
|
2
|
-
|
|
3
|
-
CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. It brings structure, consistency, and powerful developer tools to your business processes.
|
|
4
|
-
|
|
5
|
-
**Common challenges:**
|
|
6
|
-
|
|
7
|
-
- Inconsistent service object patterns across your codebase
|
|
8
|
-
- Black boxes make debugging a nightmare
|
|
9
|
-
- Fragile error handling erodes confidence
|
|
10
|
-
|
|
11
|
-
**What you get:**
|
|
12
|
-
|
|
13
|
-
- Consistent, standardized architecture
|
|
14
|
-
- Built-in flow control and error handling
|
|
15
|
-
- Composable, reusable workflows
|
|
16
|
-
- Comprehensive logging for observability
|
|
17
|
-
- Attribute validation with type coercions
|
|
18
|
-
- Sensible defaults and developer-friendly APIs
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
21
|
-
|
|
22
|
-
Add CMDx to your Gemfile:
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
gem install cmdx
|
|
26
|
-
|
|
27
|
-
# - or -
|
|
28
|
-
|
|
29
|
-
bundle add cmdx
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Configuration
|
|
33
|
-
|
|
34
|
-
For Rails applications, run the following command to generate a global configuration file in `config/initializers/cmdx.rb`.
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
rails generate cmdx:install
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
If not using Rails, manually copy the [configuration file](https://github.com/drexed/cmdx/blob/main/lib/generators/cmdx/templates/install.rb).
|
|
41
|
-
|
|
42
|
-
## The CERO Pattern
|
|
43
|
-
|
|
44
|
-
CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") pattern—a simple yet powerful approach to building reliable business logic.
|
|
45
|
-
|
|
46
|
-
```mermaid
|
|
47
|
-
flowchart LR
|
|
48
|
-
Compose --> Execute
|
|
49
|
-
Execute --> React
|
|
50
|
-
Execute -.-> Observe
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Compose
|
|
54
|
-
|
|
55
|
-
Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
class AnalyzeMetrics < CMDx::Task
|
|
59
|
-
def work
|
|
60
|
-
# Your logic here...
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Execute
|
|
66
|
-
|
|
67
|
-
Invoke tasks with a consistent API that always returns a result object. Execution automatically handles validation, type coercion, error handling, and logging. Arguments are validated and coerced before your task logic runs.
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
# Without args
|
|
71
|
-
result = AnalyzeMetrics.execute
|
|
72
|
-
|
|
73
|
-
# With args
|
|
74
|
-
result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### React
|
|
78
|
-
|
|
79
|
-
Every execution returns a result object with a clear outcome. Check the result's state (`success?`, `failed?`, `skipped?`) and access returned values, error messages, and metadata to make informed decisions.
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
if result.success?
|
|
83
|
-
# Handle success
|
|
84
|
-
elsif result.skipped?
|
|
85
|
-
# Handle skipped
|
|
86
|
-
elsif result.failed?
|
|
87
|
-
# Handle failed
|
|
88
|
-
end
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Observe
|
|
92
|
-
|
|
93
|
-
Every task execution generates structured logs with execution chains, runtime metrics, and contextual metadata. Logs can be automatically correlated using chain IDs, making it easy to trace complex workflows and debug issues.
|
|
94
|
-
|
|
95
|
-
```log
|
|
96
|
-
I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
|
|
97
|
-
index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
|
|
98
|
-
|
|
99
|
-
I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
|
|
100
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
!!! note
|
|
104
|
-
|
|
105
|
-
This represents a log-only event-sourcing approach, enabling full traceability and a complete, time-ordered view of system behavior.
|
|
106
|
-
|
|
107
|
-
## Task Generator
|
|
108
|
-
|
|
109
|
-
Generate new CMDx tasks quickly using the built-in generator:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
rails generate cmdx:task ModerateBlogPost
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
This creates a new task file with the basic structure:
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
# app/tasks/moderate_blog_post.rb
|
|
119
|
-
class ModerateBlogPost < CMDx::Task
|
|
120
|
-
def work
|
|
121
|
-
# Your logic here...
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
!!! tip
|
|
127
|
-
|
|
128
|
-
Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
|
|
129
|
-
|
|
130
|
-
## Type safety
|
|
131
|
-
|
|
132
|
-
CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
|
|
133
|
-
|
|
134
|
-
- **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf
|
|
135
|
-
- **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation
|
|
136
|
-
- **Self-documenting code** — Clear method signatures and return types
|
|
137
|
-
- **Refactoring confidence** — Type-aware refactoring reduces bugs
|
data/docs/index.md
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# CMDx
|
|
2
|
-
|
|
3
|
-
Build business logic that's powerful, predictable, and maintainable.
|
|
4
|
-
|
|
5
|
-
[](https://rubygems.org/gems/cmdx)
|
|
6
|
-
[](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
|
|
7
|
-
[](https://github.com/drexed/cmdx/blob/main/LICENSE.txt)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
Say goodbye to messy service objects. CMDx (pronounced "Command X") helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
|
|
12
|
-
|
|
13
|
-
!!! note
|
|
14
|
-
|
|
15
|
-
Documentation reflects the latest code on `main`. For version-specific documentation, please refer to the `docs/` directory within that version's tag.
|
|
16
|
-
|
|
17
|
-
## Requirements
|
|
18
|
-
|
|
19
|
-
- Ruby: MRI 3.1+ or JRuby 9.4+.
|
|
20
|
-
|
|
21
|
-
CMDx works with any Ruby framework. Rails support is built-in, but it's framework-agnostic at its core.
|
|
22
|
-
|
|
23
|
-
## Installation
|
|
24
|
-
|
|
25
|
-
```sh
|
|
26
|
-
gem install cmdx
|
|
27
|
-
|
|
28
|
-
# - or -
|
|
29
|
-
|
|
30
|
-
bundle add cmdx
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Quick Example
|
|
34
|
-
|
|
35
|
-
Build powerful business logic in four simple steps:
|
|
36
|
-
|
|
37
|
-
### 1. Compose
|
|
38
|
-
|
|
39
|
-
=== "Full Featured Task"
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
class AnalyzeMetrics < CMDx::Task
|
|
43
|
-
register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id }
|
|
44
|
-
|
|
45
|
-
on_success :track_analysis_completion!
|
|
46
|
-
|
|
47
|
-
required :dataset_id, type: :integer, numeric: { min: 1 }
|
|
48
|
-
optional :analysis_type, default: "standard"
|
|
49
|
-
|
|
50
|
-
def work
|
|
51
|
-
if dataset.nil?
|
|
52
|
-
fail!("Dataset not found", code: 404)
|
|
53
|
-
elsif dataset.unprocessed?
|
|
54
|
-
skip!("Dataset not ready for analysis")
|
|
55
|
-
else
|
|
56
|
-
context.result = PValueAnalyzer.execute(dataset:, analysis_type:)
|
|
57
|
-
context.analyzed_at = Time.now
|
|
58
|
-
|
|
59
|
-
SendAnalyzedEmail.execute(user_id: Current.account.manager_id)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def dataset
|
|
66
|
-
@dataset ||= Dataset.find_by(id: dataset_id)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def track_analysis_completion!
|
|
70
|
-
dataset.update!(analysis_result_id: context.result.id)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
=== "Minimum Viable Task"
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
class SendAnalyzedEmail < CMDx::Task
|
|
79
|
-
def work
|
|
80
|
-
user = User.find(context.user_id)
|
|
81
|
-
MetricsMailer.analyzed(user).deliver_now
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### 2. Execute
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
result = AnalyzeMetrics.execute(
|
|
90
|
-
dataset_id: 123,
|
|
91
|
-
"analysis_type" => "advanced"
|
|
92
|
-
)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. React
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
if result.success?
|
|
99
|
-
puts "Metrics analyzed at #{result.context.analyzed_at}"
|
|
100
|
-
elsif result.skipped?
|
|
101
|
-
puts "Skipping analyzation due to: #{result.reason}"
|
|
102
|
-
elsif result.failed?
|
|
103
|
-
puts "Analyzation failed due to: #{result.reason} with code #{result.metadata[:code]}"
|
|
104
|
-
end
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 4. Observe
|
|
108
|
-
|
|
109
|
-
```log
|
|
110
|
-
I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
|
|
111
|
-
index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
|
|
112
|
-
|
|
113
|
-
I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
|
|
114
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Ready to dive in? Check out the [Getting Started](getting_started.md) guide to learn more.
|
|
118
|
-
|
|
119
|
-
## Ecosystem
|
|
120
|
-
|
|
121
|
-
- [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
|
|
122
|
-
|
|
123
|
-
For backwards compatibility of certain functionality:
|
|
124
|
-
|
|
125
|
-
- [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations, `v1.5.0` - `v1.6.2`
|
|
126
|
-
- [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks, `v1.6.1` - `v1.6.2`
|
|
127
|
-
|
|
128
|
-
## Contributing
|
|
129
|
-
|
|
130
|
-
Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](https://github.com/drexed/cmdx/blob/main/CODE_OF_CONDUCT.md).
|
|
131
|
-
|
|
132
|
-
## License
|
|
133
|
-
|
|
134
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# Internationalization (i18n)
|
|
2
|
-
|
|
3
|
-
CMDx supports 90+ languages out of the box for all error messages, validations, coercions, and faults. Error messages automatically adapt to the current `I18n.locale`, making it easy to build applications for global audiences.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
All error messages are automatically localized based on your current locale:
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
class ProcessQuote < CMDx::Task
|
|
11
|
-
attribute :price, type: :float
|
|
12
|
-
|
|
13
|
-
def work
|
|
14
|
-
# Your logic here...
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
I18n.with_locale(:fr) do
|
|
19
|
-
result = ProcessQuote.execute(price: "invalid")
|
|
20
|
-
result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
|
|
21
|
-
end
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Configuration
|
|
25
|
-
|
|
26
|
-
CMDx uses the `I18n` gem for localization. In Rails, locales load automatically.
|
|
27
|
-
|
|
28
|
-
### Copy Locale Files
|
|
29
|
-
|
|
30
|
-
Copy locale files to your Rails application's `config/locales` directory:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
rails generate cmdx:locale [LOCALE]
|
|
34
|
-
|
|
35
|
-
# Eg: generate french locale
|
|
36
|
-
rails generate cmdx:locale fr
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Available Locales
|
|
40
|
-
|
|
41
|
-
- af - Afrikaans
|
|
42
|
-
- ar - Arabic
|
|
43
|
-
- az - Azerbaijani
|
|
44
|
-
- be - Belarusian
|
|
45
|
-
- bg - Bulgarian
|
|
46
|
-
- bn - Bengali
|
|
47
|
-
- bs - Bosnian
|
|
48
|
-
- ca - Catalan
|
|
49
|
-
- cnr - Montenegrin
|
|
50
|
-
- cs - Czech
|
|
51
|
-
- cy - Welsh
|
|
52
|
-
- da - Danish
|
|
53
|
-
- de - German
|
|
54
|
-
- dz - Dzongkha
|
|
55
|
-
- el - Greek
|
|
56
|
-
- en - English
|
|
57
|
-
- eo - Esperanto
|
|
58
|
-
- es - Spanish
|
|
59
|
-
- et - Estonian
|
|
60
|
-
- eu - Basque
|
|
61
|
-
- fa - Persian
|
|
62
|
-
- fi - Finnish
|
|
63
|
-
- fr - French
|
|
64
|
-
- fy - Western Frisian
|
|
65
|
-
- gd - Scottish Gaelic
|
|
66
|
-
- gl - Galician
|
|
67
|
-
- he - Hebrew
|
|
68
|
-
- hi - Hindi
|
|
69
|
-
- hr - Croatian
|
|
70
|
-
- hu - Hungarian
|
|
71
|
-
- hy - Armenian
|
|
72
|
-
- id - Indonesian
|
|
73
|
-
- is - Icelandic
|
|
74
|
-
- it - Italian
|
|
75
|
-
- ja - Japanese
|
|
76
|
-
- ka - Georgian
|
|
77
|
-
- kk - Kazakh
|
|
78
|
-
- km - Khmer
|
|
79
|
-
- kn - Kannada
|
|
80
|
-
- ko - Korean
|
|
81
|
-
- lb - Luxembourgish
|
|
82
|
-
- lo - Lao
|
|
83
|
-
- lt - Lithuanian
|
|
84
|
-
- lv - Latvian
|
|
85
|
-
- mg - Malagasy
|
|
86
|
-
- mk - Macedonian
|
|
87
|
-
- ml - Malayalam
|
|
88
|
-
- mn - Mongolian
|
|
89
|
-
- mr-IN - Marathi (India)
|
|
90
|
-
- ms - Malay
|
|
91
|
-
- nb - Norwegian Bokmål
|
|
92
|
-
- ne - Nepali
|
|
93
|
-
- nl - Dutch
|
|
94
|
-
- nn - Norwegian Nynorsk
|
|
95
|
-
- oc - Occitan
|
|
96
|
-
- or - Odia
|
|
97
|
-
- pa - Punjabi
|
|
98
|
-
- pl - Polish
|
|
99
|
-
- pt - Portuguese
|
|
100
|
-
- rm - Romansh
|
|
101
|
-
- ro - Romanian
|
|
102
|
-
- ru - Russian
|
|
103
|
-
- sc - Sardinian
|
|
104
|
-
- sk - Slovak
|
|
105
|
-
- sl - Slovenian
|
|
106
|
-
- sq - Albanian
|
|
107
|
-
- sr - Serbian
|
|
108
|
-
- st - Southern Sotho
|
|
109
|
-
- sv - Swedish
|
|
110
|
-
- sw - Swahili
|
|
111
|
-
- ta - Tamil
|
|
112
|
-
- te - Telugu
|
|
113
|
-
- th - Thai
|
|
114
|
-
- tl - Tagalog
|
|
115
|
-
- tr - Turkish
|
|
116
|
-
- tt - Tatar
|
|
117
|
-
- ug - Uyghur
|
|
118
|
-
- uk - Ukrainian
|
|
119
|
-
- ur - Urdu
|
|
120
|
-
- uz - Uzbek
|
|
121
|
-
- vi - Vietnamese
|
|
122
|
-
- wo - Wolof
|
|
123
|
-
- zh-CN - Chinese (Simplified)
|
|
124
|
-
- zh-HK - Chinese (Hong Kong)
|
|
125
|
-
- zh-TW - Chinese (Traditional)
|
|
126
|
-
- zh-YUE - Chinese (Yue)
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Interruptions - Exceptions
|
|
2
|
-
|
|
3
|
-
Exception handling differs between `execute` and `execute!`. Choose the method that matches your error handling strategy.
|
|
4
|
-
|
|
5
|
-
## Exception Handling
|
|
6
|
-
|
|
7
|
-
!!! warning "Important"
|
|
8
|
-
|
|
9
|
-
Prefer `skip!` and `fail!` over raising exceptions—they signal intent more clearly.
|
|
10
|
-
|
|
11
|
-
### Non-bang execution
|
|
12
|
-
|
|
13
|
-
Captures all exceptions and returns them as failed results:
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
class CompressDocument < CMDx::Task
|
|
17
|
-
def work
|
|
18
|
-
document = Document.find(context.document_id)
|
|
19
|
-
document.compress!
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
result = CompressDocument.execute(document_id: "unknown-doc-id")
|
|
24
|
-
result.state #=> "interrupted"
|
|
25
|
-
result.status #=> "failed"
|
|
26
|
-
result.failed? #=> true
|
|
27
|
-
result.reason #=> "[ActiveRecord::NotFoundError] record not found"
|
|
28
|
-
result.cause #=> <ActiveRecord::NotFoundError>
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
!!! note
|
|
32
|
-
|
|
33
|
-
Use `exception_handler` with `execute` to send exceptions to APM tools before they become failed results.
|
|
34
|
-
|
|
35
|
-
### Bang execution
|
|
36
|
-
|
|
37
|
-
Lets exceptions propagate naturally for standard Ruby error handling:
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
class CompressDocument < CMDx::Task
|
|
41
|
-
def work
|
|
42
|
-
document = Document.find(context.document_id)
|
|
43
|
-
document.compress!
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
begin
|
|
48
|
-
CompressDocument.execute!(document_id: "unknown-doc-id")
|
|
49
|
-
rescue ActiveRecord::NotFoundError => e
|
|
50
|
-
puts "Handle exception: #{e.message}"
|
|
51
|
-
end
|
|
52
|
-
```
|
|
@@ -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
|
-
```
|