cmdx 1.1.0 → 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 +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- 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 +390 -94
- data/docs/configuration.md +181 -65
- 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 +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- 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 +232 -281
- 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 +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
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,82 +15,150 @@ 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
|
-
|
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)`.
|
83
106
|
|
84
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
|
+
|
85
126
|
class NotificationCallback < CMDx::Callback
|
86
|
-
def initialize(channels)
|
127
|
+
def initialize(channels:, template: nil)
|
87
128
|
@channels = Array(channels)
|
129
|
+
@template = template
|
88
130
|
end
|
89
131
|
|
90
132
|
def call(task, type)
|
91
|
-
return unless type
|
133
|
+
return unless should_notify?(type)
|
92
134
|
|
93
135
|
@channels.each do |channel|
|
94
|
-
NotificationService.send(
|
136
|
+
NotificationService.send(
|
137
|
+
channel: channel,
|
138
|
+
template: @template || default_template(type),
|
139
|
+
data: extract_notification_data(task)
|
140
|
+
)
|
95
141
|
end
|
96
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
|
97
162
|
end
|
98
163
|
```
|
99
164
|
|
@@ -103,46 +168,137 @@ end
|
|
103
168
|
|
104
169
|
Execute around parameter validation:
|
105
170
|
|
106
|
-
|
107
|
-
|
171
|
+
| Callback | Timing | Description |
|
172
|
+
|----------|--------|-------------|
|
173
|
+
| `before_validation` | Before validation | Setup validation context |
|
174
|
+
| `after_validation` | After successful validation | Post-validation logic |
|
175
|
+
|
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
|
183
|
+
|
184
|
+
def call
|
185
|
+
User.create!(email: email, plan: plan)
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
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
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
108
204
|
|
109
205
|
### Execution Callbacks
|
110
206
|
|
111
207
|
Execute around task logic:
|
112
208
|
|
113
|
-
|
114
|
-
|
209
|
+
| Callback | Timing | Description |
|
210
|
+
|----------|--------|-------------|
|
211
|
+
| `before_execution` | Before `call` method | Setup and preparation |
|
212
|
+
| `after_execution` | After `call` completes | Cleanup and finalization |
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class ProcessPaymentTask < CMDx::Task
|
216
|
+
before_execution :acquire_payment_lock
|
217
|
+
after_execution :release_payment_lock
|
218
|
+
|
219
|
+
def call
|
220
|
+
Payment.process!(context.payment_data)
|
221
|
+
end
|
222
|
+
|
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
|
229
|
+
|
230
|
+
def release_payment_lock
|
231
|
+
Redis.current.del(context.lock_key) if context.lock_key
|
232
|
+
end
|
233
|
+
end
|
234
|
+
```
|
115
235
|
|
116
236
|
### State Callbacks
|
117
237
|
|
118
238
|
Execute based on execution state:
|
119
239
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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 |
|
124
246
|
|
125
247
|
### Status Callbacks
|
126
248
|
|
127
249
|
Execute based on execution status:
|
128
250
|
|
129
|
-
|
130
|
-
|
131
|
-
|
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 |
|
132
256
|
|
133
257
|
### Outcome Callbacks
|
134
258
|
|
135
259
|
Execute based on outcome classification:
|
136
260
|
|
137
|
-
|
138
|
-
|
261
|
+
| Callback | Outcomes | Description |
|
262
|
+
|----------|----------|-------------|
|
263
|
+
| `on_good` | Success or skipped | Positive outcomes |
|
264
|
+
| `on_bad` | Failed | Negative outcomes |
|
139
265
|
|
140
|
-
|
266
|
+
```ruby
|
267
|
+
class EmailCampaignTask < CMDx::Task
|
268
|
+
on_executing -> { Metrics.increment('campaigns.started') }
|
269
|
+
on_complete :track_completion
|
270
|
+
on_interrupted :handle_interruption
|
141
271
|
|
142
|
-
|
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
|
143
299
|
|
144
300
|
> [!IMPORTANT]
|
145
|
-
> 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).
|
146
302
|
|
147
303
|
```ruby
|
148
304
|
1. before_execution # Setup and preparation
|
@@ -157,94 +313,234 @@ Callbacks execute in precise order during task lifecycle:
|
|
157
313
|
10. after_execution # Cleanup and finalization
|
158
314
|
```
|
159
315
|
|
160
|
-
> [!IMPORTANT]
|
161
|
-
> Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
162
|
-
|
163
316
|
## Conditional Execution
|
164
317
|
|
165
|
-
|
318
|
+
> [!TIP]
|
319
|
+
> Use `:if` and `:unless` options for conditional callback execution. Conditions can be method names, procs, or strings.
|
166
320
|
|
167
|
-
| Option
|
168
|
-
|
169
|
-
| `:if`
|
170
|
-
| `: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?` |
|
171
325
|
|
172
326
|
```ruby
|
173
|
-
class
|
174
|
-
# Method name
|
327
|
+
class ProcessOrderTask < CMDx::Task
|
328
|
+
# Method name conditions
|
175
329
|
on_success :send_receipt, if: :email_enabled?
|
330
|
+
on_failed :retry_payment, unless: :max_retries_reached?
|
176
331
|
|
177
|
-
# Proc
|
178
|
-
|
332
|
+
# Proc conditions
|
333
|
+
after_execution :log_metrics, if: -> { Rails.env.production? }
|
334
|
+
on_success :expensive_operation, unless: -> { SystemStatus.overloaded? }
|
179
335
|
|
180
|
-
# String
|
181
|
-
|
336
|
+
# String conditions (evaluated as methods)
|
337
|
+
on_complete :update_analytics, if: "tracking_enabled?"
|
182
338
|
|
183
339
|
# Multiple conditions
|
184
|
-
|
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
|
185
348
|
|
186
349
|
private
|
187
350
|
|
188
351
|
def email_enabled?
|
189
|
-
context.user.email_notifications?
|
352
|
+
context.user.email_notifications? && !context.user.email.blank?
|
190
353
|
end
|
191
354
|
|
192
|
-
def
|
193
|
-
|
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
|
194
361
|
end
|
195
362
|
|
196
|
-
def
|
197
|
-
|
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
|
397
|
+
end
|
398
|
+
|
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
|
405
|
+
end
|
406
|
+
|
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)
|
198
451
|
end
|
199
452
|
end
|
200
453
|
```
|
201
454
|
|
202
455
|
## Callback Inheritance
|
203
456
|
|
204
|
-
|
457
|
+
> [!NOTE]
|
458
|
+
> Callbacks are inherited from parent classes, enabling application-wide patterns. Child classes can add additional callbacks or override inherited behavior.
|
205
459
|
|
206
460
|
```ruby
|
207
461
|
class ApplicationTask < CMDx::Task
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
212
472
|
|
213
473
|
private
|
214
474
|
|
215
475
|
def log_task_start
|
216
|
-
Rails.logger.info "Starting #{self.class.name}"
|
476
|
+
Rails.logger.info "Starting #{self.class.name} with context: #{context.to_h.except(:sensitive_data)}"
|
217
477
|
end
|
218
478
|
|
219
479
|
def log_task_end
|
220
|
-
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}"
|
221
481
|
end
|
222
482
|
|
223
483
|
def report_failure
|
224
|
-
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
|
+
)
|
225
490
|
end
|
226
491
|
|
227
492
|
def track_success_metrics
|
228
493
|
Metrics.increment("task.#{self.class.name.underscore}.success")
|
229
494
|
end
|
495
|
+
|
496
|
+
def track_execution_metrics
|
497
|
+
Metrics.histogram("task.#{self.class.name.underscore}.runtime", result.runtime)
|
498
|
+
end
|
230
499
|
end
|
231
500
|
|
232
|
-
class
|
233
|
-
|
234
|
-
|
235
|
-
|
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?
|
236
508
|
|
237
509
|
def call
|
238
|
-
# Inherits
|
239
|
-
|
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
|
+
)
|
240
539
|
end
|
241
540
|
end
|
242
541
|
```
|
243
542
|
|
244
|
-
> [!TIP]
|
245
|
-
> Callbacks are inherited by subclasses, making them ideal for setting up global lifecycle patterns across all tasks in your application.
|
246
|
-
|
247
543
|
---
|
248
544
|
|
249
545
|
- **Prev:** [Parameters - Defaults](parameters/defaults.md)
|
250
|
-
- **
|
546
|
+
- **Next:** [Middlewares](middlewares.md)
|