cmdx 1.0.1 → 1.1.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/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +21 -0
- data/.cursor/prompts/yardoc.md +13 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +29 -3
- data/README.md +2 -1
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +126 -60
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +382 -119
- data/docs/configuration.md +211 -49
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +152 -127
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +183 -120
- data/docs/middlewares.md +165 -392
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +251 -289
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +247 -159
- data/docs/testing.md +196 -203
- data/docs/workflows.md +146 -101
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +39 -55
- data/lib/cmdx/callback_registry.rb +80 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +23 -116
- data/lib/cmdx/chain_serializer.rb +34 -146
- data/lib/cmdx/coercion.rb +57 -0
- data/lib/cmdx/coercion_registry.rb +113 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +101 -162
- data/lib/cmdx/context.rb +34 -166
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +59 -154
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +153 -216
- data/lib/cmdx/fault.rb +68 -150
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -110
- data/lib/cmdx/lazy_struct.rb +110 -186
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +22 -79
- data/lib/cmdx/logger_ansi.rb +31 -72
- data/lib/cmdx/logger_serializer.rb +74 -103
- data/lib/cmdx/middleware.rb +56 -60
- data/lib/cmdx/middleware_registry.rb +82 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +167 -183
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +37 -55
- data/lib/cmdx/parameter_registry.rb +65 -84
- data/lib/cmdx/parameter_serializer.rb +32 -76
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -259
- data/lib/cmdx/result_ansi.rb +28 -80
- data/lib/cmdx/result_inspector.rb +34 -70
- data/lib/cmdx/result_logger.rb +23 -77
- data/lib/cmdx/result_serializer.rb +59 -125
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +336 -427
- data/lib/cmdx/task_deprecator.rb +52 -0
- data/lib/cmdx/task_processor.rb +246 -0
- data/lib/cmdx/task_serializer.rb +34 -69
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +57 -0
- data/lib/cmdx/validator_registry.rb +108 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +58 -330
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +36 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/docs/callbacks.md
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
# Callbacks
|
2
2
|
|
3
|
-
Callbacks
|
4
|
-
specific transition points. Callback callables have access to the same context and result information
|
5
|
-
as the `call` method, enabling rich integration patterns.
|
3
|
+
Callbacks provide precise control over task execution lifecycle, running custom logic at specific transition points. Callback callables have access to the same context and result information as the `call` method, enabling rich integration patterns.
|
6
4
|
|
7
5
|
## Table of Contents
|
8
6
|
|
9
7
|
- [TLDR](#tldr)
|
10
|
-
- [Overview](#overview)
|
11
8
|
- [Callback Declaration](#callback-declaration)
|
12
9
|
- [Callback Classes](#callback-classes)
|
13
10
|
- [Available Callbacks](#available-callbacks)
|
@@ -18,164 +15,290 @@ as the `call` method, enabling rich integration patterns.
|
|
18
15
|
- [Outcome Callbacks](#outcome-callbacks)
|
19
16
|
- [Execution Order](#execution-order)
|
20
17
|
- [Conditional Execution](#conditional-execution)
|
18
|
+
- [Error Handling](#error-handling)
|
21
19
|
- [Callback Inheritance](#callback-inheritance)
|
22
20
|
|
23
21
|
## TLDR
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
- **Conditional** - Support `:if` and `:unless` options for conditional execution
|
30
|
-
- **Inheritance** - Callbacks are inherited, perfect for global patterns
|
23
|
+
```ruby
|
24
|
+
# Method name callbacks
|
25
|
+
after_validation :verify_order_data
|
26
|
+
on_success :send_notification
|
31
27
|
|
32
|
-
|
33
|
-
|
28
|
+
# Proc/lambda callbacks
|
29
|
+
on_complete -> { send_telemetry_data }
|
34
30
|
|
35
|
-
|
31
|
+
# Callback class instances
|
32
|
+
before_execution LoggingCallback.new(:debug)
|
36
33
|
|
37
|
-
|
34
|
+
# Conditional execution
|
35
|
+
on_failed :alert_support, if: :critical_order?
|
36
|
+
after_execution :cleanup, unless: :preserve_data?
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
after_validation :verify_order_data
|
38
|
+
# Multiple callbacks for same event
|
39
|
+
on_success :increment_counter, :send_notification
|
40
|
+
```
|
43
41
|
|
44
|
-
|
45
|
-
|
42
|
+
> [!IMPORTANT]
|
43
|
+
> Callbacks execute in declaration order (FIFO) and are inherited by subclasses, making them ideal for application-wide patterns.
|
44
|
+
|
45
|
+
## Callback Declaration
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
on_success NotificationCallback.new([:email, :slack])
|
47
|
+
> [!NOTE]
|
48
|
+
> Callbacks can be declared using method names, procs/lambdas, Callback class instances, or blocks. All forms have access to the task's context and result.
|
50
49
|
|
51
|
-
|
52
|
-
on_success :increment_counter, :send_notification
|
50
|
+
### Declaration Methods
|
53
51
|
|
54
|
-
|
55
|
-
|
56
|
-
|
52
|
+
| Method | Description | Example |
|
53
|
+
|--------|-------------|---------|
|
54
|
+
| Method name | References instance method | `on_success :send_email` |
|
55
|
+
| Proc/Lambda | Inline callable | `on_failed -> { alert_team }` |
|
56
|
+
| Callback class | Reusable class instance | `before_execution LoggerCallback.new` |
|
57
|
+
| Block | Inline block | `on_success { increment_counter }` |
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
```ruby
|
60
|
+
class ProcessOrderTask < CMDx::Task
|
61
|
+
# Method name
|
62
|
+
before_validation :load_order
|
63
|
+
after_validation :verify_inventory
|
64
|
+
|
65
|
+
# Proc/lambda
|
66
|
+
on_executing -> { context.start_time = Time.current }
|
67
|
+
on_complete lambda { Metrics.increment('orders.processed') }
|
68
|
+
|
69
|
+
# Callback class
|
70
|
+
before_execution AuditCallback.new(action: :process_order)
|
71
|
+
on_success NotificationCallback.new(channels: [:email, :slack])
|
72
|
+
|
73
|
+
# Block
|
74
|
+
on_failed do
|
75
|
+
ErrorReporter.notify(
|
76
|
+
error: result.metadata[:error],
|
77
|
+
order_id: context.order_id,
|
78
|
+
user_id: context.user_id
|
79
|
+
)
|
61
80
|
end
|
62
81
|
|
82
|
+
# Multiple callbacks
|
83
|
+
on_success :update_inventory, :send_confirmation, :log_success
|
84
|
+
|
63
85
|
def call
|
64
|
-
context.order = Order.find(order_id)
|
86
|
+
context.order = Order.find(context.order_id)
|
65
87
|
context.order.process!
|
66
88
|
end
|
67
89
|
|
68
90
|
private
|
69
91
|
|
70
|
-
def
|
71
|
-
context.order
|
92
|
+
def load_order
|
93
|
+
context.order ||= Order.find(context.order_id)
|
72
94
|
end
|
73
95
|
|
74
|
-
def
|
75
|
-
|
96
|
+
def verify_inventory
|
97
|
+
raise "Insufficient inventory" unless context.order.items_available?
|
76
98
|
end
|
77
99
|
end
|
78
100
|
```
|
79
101
|
|
80
102
|
## Callback Classes
|
81
103
|
|
82
|
-
|
83
|
-
|
84
|
-
### Creating Callback Classes
|
104
|
+
> [!TIP]
|
105
|
+
> Create reusable Callback classes for complex logic or cross-cutting concerns. Callback classes inherit from `CMDx::Callback` and implement `call(task, type)`.
|
85
106
|
|
86
107
|
```ruby
|
108
|
+
class AuditCallback < CMDx::Callback
|
109
|
+
def initialize(action:, level: :info)
|
110
|
+
@action = action
|
111
|
+
@level = level
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(task, type)
|
115
|
+
AuditLogger.log(
|
116
|
+
level: @level,
|
117
|
+
action: @action,
|
118
|
+
task: task.class.name,
|
119
|
+
callback_type: type,
|
120
|
+
user_id: task.context.current_user&.id,
|
121
|
+
timestamp: Time.current
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
87
126
|
class NotificationCallback < CMDx::Callback
|
88
|
-
def initialize(channels)
|
127
|
+
def initialize(channels:, template: nil)
|
89
128
|
@channels = Array(channels)
|
129
|
+
@template = template
|
90
130
|
end
|
91
131
|
|
92
|
-
def call(task,
|
93
|
-
return unless
|
132
|
+
def call(task, type)
|
133
|
+
return unless should_notify?(type)
|
94
134
|
|
95
135
|
@channels.each do |channel|
|
96
|
-
NotificationService.send(
|
136
|
+
NotificationService.send(
|
137
|
+
channel: channel,
|
138
|
+
template: @template || default_template(type),
|
139
|
+
data: extract_notification_data(task)
|
140
|
+
)
|
97
141
|
end
|
98
142
|
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def should_notify?(type)
|
147
|
+
%i[on_success on_failed].include?(type)
|
148
|
+
end
|
149
|
+
|
150
|
+
def default_template(type)
|
151
|
+
type == :on_success ? :task_success : :task_failure
|
152
|
+
end
|
153
|
+
|
154
|
+
def extract_notification_data(task)
|
155
|
+
{
|
156
|
+
task_name: task.class.name,
|
157
|
+
status: task.result.status,
|
158
|
+
runtime: task.result.runtime,
|
159
|
+
context: task.context.to_h.except(:sensitive_data)
|
160
|
+
}
|
161
|
+
end
|
99
162
|
end
|
100
163
|
```
|
101
164
|
|
102
|
-
|
165
|
+
## Available Callbacks
|
103
166
|
|
104
|
-
|
167
|
+
### Validation Callbacks
|
105
168
|
|
106
|
-
|
107
|
-
class ProcessOrderTask < CMDx::Task
|
108
|
-
# Recommended: Use the register class method
|
109
|
-
register :before_execution, LoggingCallback.new(:debug)
|
110
|
-
register :on_success, NotificationCallback.new([:email, :slack])
|
111
|
-
register :on_failure, :alert_admin, if: :critical?
|
169
|
+
Execute around parameter validation:
|
112
170
|
|
113
|
-
|
114
|
-
|
171
|
+
| Callback | Timing | Description |
|
172
|
+
|----------|--------|-------------|
|
173
|
+
| `before_validation` | Before validation | Setup validation context |
|
174
|
+
| `after_validation` | After successful validation | Post-validation logic |
|
115
175
|
|
116
|
-
|
117
|
-
|
118
|
-
|
176
|
+
```ruby
|
177
|
+
class CreateUserTask < CMDx::Task
|
178
|
+
before_validation :normalize_email
|
179
|
+
after_validation :check_user_limits
|
180
|
+
|
181
|
+
required :email, type: :string
|
182
|
+
required :plan, type: :string
|
119
183
|
|
120
184
|
def call
|
121
|
-
|
122
|
-
context.order.process!
|
185
|
+
User.create!(email: email, plan: plan)
|
123
186
|
end
|
124
187
|
|
125
188
|
private
|
126
189
|
|
127
|
-
def
|
128
|
-
context.
|
190
|
+
def normalize_email
|
191
|
+
context.email = email.downcase.strip
|
192
|
+
end
|
193
|
+
|
194
|
+
def check_user_limits
|
195
|
+
current_users = User.where(plan: plan).count
|
196
|
+
plan_limit = Plan.find_by(name: plan).user_limit
|
197
|
+
|
198
|
+
if current_users >= plan_limit
|
199
|
+
throw(:skip, reason: "Plan user limit reached")
|
200
|
+
end
|
129
201
|
end
|
130
202
|
end
|
131
203
|
```
|
132
204
|
|
133
|
-
|
205
|
+
### Execution Callbacks
|
134
206
|
|
135
|
-
|
207
|
+
Execute around task logic:
|
136
208
|
|
137
|
-
|
209
|
+
| Callback | Timing | Description |
|
210
|
+
|----------|--------|-------------|
|
211
|
+
| `before_execution` | Before `call` method | Setup and preparation |
|
212
|
+
| `after_execution` | After `call` completes | Cleanup and finalization |
|
138
213
|
|
139
|
-
|
140
|
-
|
214
|
+
```ruby
|
215
|
+
class ProcessPaymentTask < CMDx::Task
|
216
|
+
before_execution :acquire_payment_lock
|
217
|
+
after_execution :release_payment_lock
|
141
218
|
|
142
|
-
|
219
|
+
def call
|
220
|
+
Payment.process!(context.payment_data)
|
221
|
+
end
|
143
222
|
|
144
|
-
|
223
|
+
private
|
224
|
+
|
225
|
+
def acquire_payment_lock
|
226
|
+
context.lock_key = "payment:#{context.payment_id}"
|
227
|
+
Redis.current.set(context.lock_key, "locked", ex: 300)
|
228
|
+
end
|
145
229
|
|
146
|
-
|
147
|
-
|
230
|
+
def release_payment_lock
|
231
|
+
Redis.current.del(context.lock_key) if context.lock_key
|
232
|
+
end
|
233
|
+
end
|
234
|
+
```
|
148
235
|
|
149
236
|
### State Callbacks
|
150
237
|
|
151
238
|
Execute based on execution state:
|
152
239
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
240
|
+
| Callback | Condition | Description |
|
241
|
+
|----------|-----------|-------------|
|
242
|
+
| `on_executing` | Task begins running | Track execution start |
|
243
|
+
| `on_complete` | Task completes successfully | Handle successful completion |
|
244
|
+
| `on_interrupted` | Task is halted (skip/failure) | Handle interruptions |
|
245
|
+
| `on_executed` | Task finishes (any outcome) | Post-execution logic |
|
157
246
|
|
158
247
|
### Status Callbacks
|
159
248
|
|
160
249
|
Execute based on execution status:
|
161
250
|
|
162
|
-
|
163
|
-
|
164
|
-
|
251
|
+
| Callback | Status | Description |
|
252
|
+
|----------|--------|-------------|
|
253
|
+
| `on_success` | Task succeeds | Handle success |
|
254
|
+
| `on_skipped` | Task is skipped | Handle skips |
|
255
|
+
| `on_failed` | Task fails | Handle failures |
|
165
256
|
|
166
257
|
### Outcome Callbacks
|
167
258
|
|
168
259
|
Execute based on outcome classification:
|
169
260
|
|
170
|
-
|
171
|
-
|
261
|
+
| Callback | Outcomes | Description |
|
262
|
+
|----------|----------|-------------|
|
263
|
+
| `on_good` | Success or skipped | Positive outcomes |
|
264
|
+
| `on_bad` | Failed | Negative outcomes |
|
172
265
|
|
173
|
-
|
266
|
+
```ruby
|
267
|
+
class EmailCampaignTask < CMDx::Task
|
268
|
+
on_executing -> { Metrics.increment('campaigns.started') }
|
269
|
+
on_complete :track_completion
|
270
|
+
on_interrupted :handle_interruption
|
174
271
|
|
175
|
-
|
272
|
+
on_success :schedule_followup
|
273
|
+
on_skipped :log_skip_reason
|
274
|
+
on_failed :alert_marketing_team
|
275
|
+
|
276
|
+
on_good -> { Metrics.increment('campaigns.positive_outcome') }
|
277
|
+
on_bad :create_incident_ticket
|
278
|
+
|
279
|
+
def call
|
280
|
+
EmailService.send_campaign(context.campaign_data)
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
def track_completion
|
286
|
+
Campaign.find(context.campaign_id).update!(
|
287
|
+
sent_at: Time.current,
|
288
|
+
recipient_count: context.recipients.size
|
289
|
+
)
|
290
|
+
end
|
291
|
+
|
292
|
+
def handle_interruption
|
293
|
+
Campaign.find(context.campaign_id).update!(status: :interrupted)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
297
|
+
|
298
|
+
## Execution Order
|
176
299
|
|
177
300
|
> [!IMPORTANT]
|
178
|
-
> Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
301
|
+
> Callbacks execute in precise lifecycle order. Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
179
302
|
|
180
303
|
```ruby
|
181
304
|
1. before_execution # Setup and preparation
|
@@ -190,94 +313,234 @@ Callbacks execute in precise order during task lifecycle:
|
|
190
313
|
10. after_execution # Cleanup and finalization
|
191
314
|
```
|
192
315
|
|
193
|
-
> [!IMPORTANT]
|
194
|
-
> Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
195
|
-
|
196
316
|
## Conditional Execution
|
197
317
|
|
198
|
-
|
318
|
+
> [!TIP]
|
319
|
+
> Use `:if` and `:unless` options for conditional callback execution. Conditions can be method names, procs, or strings.
|
199
320
|
|
200
|
-
| Option
|
201
|
-
|
202
|
-
| `:if`
|
203
|
-
| `:unless` | Execute
|
321
|
+
| Option | Description | Example |
|
322
|
+
|--------|-------------|---------|
|
323
|
+
| `:if` | Execute if condition is truthy | `if: :production_env?` |
|
324
|
+
| `:unless` | Execute if condition is falsy | `unless: :maintenance_mode?` |
|
204
325
|
|
205
326
|
```ruby
|
206
|
-
class
|
207
|
-
# Method name
|
327
|
+
class ProcessOrderTask < CMDx::Task
|
328
|
+
# Method name conditions
|
208
329
|
on_success :send_receipt, if: :email_enabled?
|
330
|
+
on_failed :retry_payment, unless: :max_retries_reached?
|
209
331
|
|
210
|
-
# Proc
|
211
|
-
|
332
|
+
# Proc conditions
|
333
|
+
after_execution :log_metrics, if: -> { Rails.env.production? }
|
334
|
+
on_success :expensive_operation, unless: -> { SystemStatus.overloaded? }
|
212
335
|
|
213
|
-
# String
|
214
|
-
|
336
|
+
# String conditions (evaluated as methods)
|
337
|
+
on_complete :update_analytics, if: "tracking_enabled?"
|
215
338
|
|
216
339
|
# Multiple conditions
|
217
|
-
|
340
|
+
on_failed :escalate_to_support, if: :critical_order?, unless: :business_hours?
|
341
|
+
|
342
|
+
# Complex conditional logic
|
343
|
+
on_success :trigger_automation, if: :automation_conditions_met?
|
344
|
+
|
345
|
+
def call
|
346
|
+
Order.process!(context.order_data)
|
347
|
+
end
|
218
348
|
|
219
349
|
private
|
220
350
|
|
221
351
|
def email_enabled?
|
222
|
-
context.user.email_notifications?
|
352
|
+
context.user.email_notifications? && !context.user.email.blank?
|
353
|
+
end
|
354
|
+
|
355
|
+
def max_retries_reached?
|
356
|
+
context.retry_count >= 3
|
357
|
+
end
|
358
|
+
|
359
|
+
def critical_order?
|
360
|
+
context.order_value > 10_000 || context.priority == :high
|
361
|
+
end
|
362
|
+
|
363
|
+
def business_hours?
|
364
|
+
Time.current.hour.between?(9, 17) && Time.current.weekday?
|
365
|
+
end
|
366
|
+
|
367
|
+
def automation_conditions_met?
|
368
|
+
context.order_type == :subscription &&
|
369
|
+
context.user.plan.automation_enabled? &&
|
370
|
+
!SystemStatus.maintenance_mode?
|
371
|
+
end
|
372
|
+
end
|
373
|
+
```
|
374
|
+
|
375
|
+
## Error Handling
|
376
|
+
|
377
|
+
> [!WARNING]
|
378
|
+
> Callback errors can interrupt task execution. Use proper error handling and consider callback isolation for non-critical operations.
|
379
|
+
|
380
|
+
### Callback Error Behavior
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
class ProcessDataTask < CMDx::Task
|
384
|
+
before_execution :critical_setup # Error stops execution
|
385
|
+
on_success :send_notification # Error stops callback chain
|
386
|
+
after_execution :cleanup_resources # Always runs
|
387
|
+
|
388
|
+
def call
|
389
|
+
ProcessingService.handle(context.data)
|
390
|
+
end
|
391
|
+
|
392
|
+
private
|
393
|
+
|
394
|
+
def critical_setup
|
395
|
+
# Critical callback - let errors bubble up
|
396
|
+
context.processor = ProcessorService.initialize_secure_processor
|
223
397
|
end
|
224
398
|
|
225
|
-
def
|
226
|
-
|
399
|
+
def send_notification
|
400
|
+
# Non-critical callback - handle errors gracefully
|
401
|
+
NotificationService.send(context.notification_data)
|
402
|
+
rescue NotificationService::Error => e
|
403
|
+
Rails.logger.warn "Notification failed: #{e.message}"
|
404
|
+
# Don't re-raise - allow other callbacks to continue
|
227
405
|
end
|
228
406
|
|
229
|
-
def
|
230
|
-
|
407
|
+
def cleanup_resources
|
408
|
+
# Cleanup callback - always handle errors
|
409
|
+
context.processor&.cleanup
|
410
|
+
rescue => e
|
411
|
+
Rails.logger.error "Cleanup failed: #{e.message}"
|
412
|
+
# Log but don't re-raise
|
413
|
+
end
|
414
|
+
end
|
415
|
+
```
|
416
|
+
|
417
|
+
### Isolating Non-Critical Callbacks
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
class ResilientCallback < CMDx::Callback
|
421
|
+
def initialize(callback_proc, isolate: false)
|
422
|
+
@callback_proc = callback_proc
|
423
|
+
@isolate = isolate
|
424
|
+
end
|
425
|
+
|
426
|
+
def call(task, type)
|
427
|
+
if @isolate
|
428
|
+
begin
|
429
|
+
@callback_proc.call(task, type)
|
430
|
+
rescue => e
|
431
|
+
Rails.logger.warn "Isolated callback failed: #{e.message}"
|
432
|
+
end
|
433
|
+
else
|
434
|
+
@callback_proc.call(task, type)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
class ProcessOrderTask < CMDx::Task
|
440
|
+
# Critical callback
|
441
|
+
before_execution :validate_payment_method
|
442
|
+
|
443
|
+
# Isolated non-critical callback
|
444
|
+
on_success ResilientCallback.new(
|
445
|
+
-> (task, type) { AnalyticsService.track_order(task.context.order_id) },
|
446
|
+
isolate: true
|
447
|
+
)
|
448
|
+
|
449
|
+
def call
|
450
|
+
Order.process!(context.order_data)
|
231
451
|
end
|
232
452
|
end
|
233
453
|
```
|
234
454
|
|
235
455
|
## Callback Inheritance
|
236
456
|
|
237
|
-
|
457
|
+
> [!NOTE]
|
458
|
+
> Callbacks are inherited from parent classes, enabling application-wide patterns. Child classes can add additional callbacks or override inherited behavior.
|
238
459
|
|
239
460
|
```ruby
|
240
461
|
class ApplicationTask < CMDx::Task
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
462
|
+
# Global logging
|
463
|
+
before_execution :log_task_start
|
464
|
+
after_execution :log_task_end
|
465
|
+
|
466
|
+
# Global error handling
|
467
|
+
on_failed :report_failure
|
468
|
+
|
469
|
+
# Global metrics
|
470
|
+
on_success :track_success_metrics
|
471
|
+
on_executed :track_execution_metrics
|
245
472
|
|
246
473
|
private
|
247
474
|
|
248
475
|
def log_task_start
|
249
|
-
Rails.logger.info "Starting #{self.class.name}"
|
476
|
+
Rails.logger.info "Starting #{self.class.name} with context: #{context.to_h.except(:sensitive_data)}"
|
250
477
|
end
|
251
478
|
|
252
479
|
def log_task_end
|
253
|
-
Rails.logger.info "Finished #{self.class.name} in #{result.runtime}
|
480
|
+
Rails.logger.info "Finished #{self.class.name} in #{result.runtime}ms with status: #{result.status}"
|
254
481
|
end
|
255
482
|
|
256
483
|
def report_failure
|
257
|
-
ErrorReporter.notify(
|
484
|
+
ErrorReporter.notify(
|
485
|
+
task: self.class.name,
|
486
|
+
error: result.metadata[:reason],
|
487
|
+
context: context.to_h.except(:sensitive_data),
|
488
|
+
backtrace: result.metadata[:backtrace]
|
489
|
+
)
|
258
490
|
end
|
259
491
|
|
260
492
|
def track_success_metrics
|
261
493
|
Metrics.increment("task.#{self.class.name.underscore}.success")
|
262
494
|
end
|
495
|
+
|
496
|
+
def track_execution_metrics
|
497
|
+
Metrics.histogram("task.#{self.class.name.underscore}.runtime", result.runtime)
|
498
|
+
end
|
263
499
|
end
|
264
500
|
|
265
|
-
class
|
266
|
-
|
267
|
-
|
268
|
-
|
501
|
+
class ProcessPaymentTask < ApplicationTask
|
502
|
+
# Inherits all ApplicationTask callbacks
|
503
|
+
# Plus payment-specific callbacks
|
504
|
+
|
505
|
+
before_validation :load_payment_method
|
506
|
+
on_success :send_receipt
|
507
|
+
on_failed :refund_payment, if: :payment_captured?
|
269
508
|
|
270
509
|
def call
|
271
|
-
# Inherits
|
272
|
-
|
510
|
+
# Inherits global logging, error handling, and metrics
|
511
|
+
# Plus payment-specific behavior
|
512
|
+
PaymentProcessor.charge(context.payment_data)
|
513
|
+
end
|
514
|
+
|
515
|
+
private
|
516
|
+
|
517
|
+
def load_payment_method
|
518
|
+
context.payment_method = PaymentMethod.find(context.payment_method_id)
|
519
|
+
end
|
520
|
+
|
521
|
+
def send_receipt
|
522
|
+
ReceiptService.send(
|
523
|
+
user: context.user,
|
524
|
+
payment: context.payment,
|
525
|
+
template: :payment_success
|
526
|
+
)
|
527
|
+
end
|
528
|
+
|
529
|
+
def payment_captured?
|
530
|
+
context.payment&.status == :captured
|
531
|
+
end
|
532
|
+
|
533
|
+
def refund_payment
|
534
|
+
RefundService.process(
|
535
|
+
payment: context.payment,
|
536
|
+
reason: :task_failure,
|
537
|
+
amount: context.payment.amount
|
538
|
+
)
|
273
539
|
end
|
274
540
|
end
|
275
541
|
```
|
276
542
|
|
277
|
-
> [!TIP]
|
278
|
-
> Callbacks are inherited by subclasses, making them ideal for setting up global lifecycle patterns across all tasks in your application.
|
279
|
-
|
280
543
|
---
|
281
544
|
|
282
545
|
- **Prev:** [Parameters - Defaults](parameters/defaults.md)
|
283
|
-
- **
|
546
|
+
- **Next:** [Middlewares](middlewares.md)
|