cmdx 1.10.0 β 1.10.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/.yardopts +7 -0
- data/CHANGELOG.md +9 -0
- data/Rakefile +21 -0
- data/docs/configuration.md +314 -0
- data/docs/getting_started.md +37 -300
- data/docs/index.md +4 -2
- data/docs/retries.md +1 -1
- data/docs/stylesheets/extra.css +26 -26
- data/docs/tips_and_tricks.md +2 -1
- data/examples/sidekiq_async_execution.md +29 -0
- data/lib/cmdx/executor.rb +0 -7
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +65 -19
- metadata +18 -2
- data/LLM.md +0 -3727
data/LLM.md
DELETED
|
@@ -1,3727 +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 it solves:**
|
|
6
|
-
|
|
7
|
-
- Inconsistent service object patterns across your codebase
|
|
8
|
-
- Limited logging makes 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
|
-
## The CERO Pattern
|
|
21
|
-
|
|
22
|
-
CMDx embraces the Compose, Execute, React, Observe (CERO) patternβa simple yet powerful approach to building reliable business logic.
|
|
23
|
-
|
|
24
|
-
π§© **Compose** β Define small, focused tasks with typed attributes and validations
|
|
25
|
-
|
|
26
|
-
β‘ **Execute** β Run tasks with clear outcomes and pluggable behaviors
|
|
27
|
-
|
|
28
|
-
π **React** β Adapt to outcomes by chaining follow-up tasks or handling faults
|
|
29
|
-
|
|
30
|
-
π **Observe** β Capture structured logs and execution chains for debugging
|
|
31
|
-
|
|
32
|
-
## Installation
|
|
33
|
-
|
|
34
|
-
Add CMDx to your Gemfile:
|
|
35
|
-
|
|
36
|
-
```ruby
|
|
37
|
-
gem 'cmdx'
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
For Rails applications, generate the configuration:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
rails generate cmdx:install
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
This creates `config/initializers/cmdx.rb` file.
|
|
47
|
-
|
|
48
|
-
## Configuration Hierarchy
|
|
49
|
-
|
|
50
|
-
CMDx uses a straightforward two-tier configuration system:
|
|
51
|
-
|
|
52
|
-
1. **Global Configuration** β Framework-wide defaults
|
|
53
|
-
2. **Task Settings** β Class-level overrides using `settings`
|
|
54
|
-
|
|
55
|
-
!!! warning "Important"
|
|
56
|
-
|
|
57
|
-
Task settings take precedence over global config. Settings are inherited from parent classes and can be overridden in subclasses.
|
|
58
|
-
|
|
59
|
-
## Global Configuration
|
|
60
|
-
|
|
61
|
-
Configure framework-wide defaults that apply to all tasks. These settings come with sensible defaults out of the box.
|
|
62
|
-
|
|
63
|
-
### Breakpoints
|
|
64
|
-
|
|
65
|
-
Control when `execute!` raises a `CMDx::Fault` based on task status.
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
CMDx.configure do |config|
|
|
69
|
-
config.task_breakpoints = "failed" # String or Array[String]
|
|
70
|
-
end
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
For workflows, configure which statuses halt the execution pipeline:
|
|
74
|
-
|
|
75
|
-
```ruby
|
|
76
|
-
CMDx.configure do |config|
|
|
77
|
-
config.workflow_breakpoints = ["skipped", "failed"]
|
|
78
|
-
end
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Rollback
|
|
82
|
-
|
|
83
|
-
Control when a `rollback` of task execution is called.
|
|
84
|
-
|
|
85
|
-
```ruby
|
|
86
|
-
CMDx.configure do |config|
|
|
87
|
-
config.rollback_on = ["failed"] # String or Array[String]
|
|
88
|
-
end
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Backtraces
|
|
92
|
-
|
|
93
|
-
Enable detailed backtraces for non-fault exceptions to improve debugging. Optionally clean up stack traces to remove framework noise.
|
|
94
|
-
|
|
95
|
-
!!! note
|
|
96
|
-
|
|
97
|
-
In Rails environments, `backtrace_cleaner` defaults to `Rails.backtrace_cleaner.clean`.
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
CMDx.configure do |config|
|
|
101
|
-
# Truthy
|
|
102
|
-
config.backtrace = true
|
|
103
|
-
|
|
104
|
-
# Via callable (must respond to `call(backtrace)`)
|
|
105
|
-
config.backtrace_cleaner = AdvanceCleaner.new
|
|
106
|
-
|
|
107
|
-
# Via proc or lambda
|
|
108
|
-
config.backtrace_cleaner = ->(backtrace) { backtrace[0..5] }
|
|
109
|
-
end
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Exception Handlers
|
|
113
|
-
|
|
114
|
-
Register handlers that run when non-fault exceptions occur.
|
|
115
|
-
|
|
116
|
-
!!! tip
|
|
117
|
-
|
|
118
|
-
Use exception handlers to send errors to your APM of choice.
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
CMDx.configure do |config|
|
|
122
|
-
# Via callable (must respond to `call(task, exception)`)
|
|
123
|
-
config.exception_handler = NewRelicReporter
|
|
124
|
-
|
|
125
|
-
# Via proc or lambda
|
|
126
|
-
config.exception_handler = proc do |task, exception|
|
|
127
|
-
APMService.report(exception, extra_data: { task: task.name, id: task.id })
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Logging
|
|
133
|
-
|
|
134
|
-
```ruby
|
|
135
|
-
CMDx.configure do |config|
|
|
136
|
-
config.logger = CustomLogger.new($stdout)
|
|
137
|
-
end
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Middlewares
|
|
141
|
-
|
|
142
|
-
See the [Middlewares](https://github.com/drexed/cmdx/blob/main/docs/middlewares.md#declarations) docs for task level configurations.
|
|
143
|
-
|
|
144
|
-
```ruby
|
|
145
|
-
CMDx.configure do |config|
|
|
146
|
-
# Via callable (must respond to `call(task, options)`)
|
|
147
|
-
config.middlewares.register CMDx::Middlewares::Timeout
|
|
148
|
-
|
|
149
|
-
# Via proc or lambda
|
|
150
|
-
config.middlewares.register proc { |task, options|
|
|
151
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
152
|
-
result = yield
|
|
153
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
154
|
-
Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" }
|
|
155
|
-
result
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
# With options
|
|
159
|
-
config.middlewares.register AuditTrailMiddleware, service_name: "document_processor"
|
|
160
|
-
|
|
161
|
-
# Remove middleware
|
|
162
|
-
config.middlewares.deregister CMDx::Middlewares::Timeout
|
|
163
|
-
end
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
!!! note
|
|
167
|
-
|
|
168
|
-
Middlewares are executed in registration order. Each middleware wraps the next, creating an execution chain around task logic.
|
|
169
|
-
|
|
170
|
-
### Callbacks
|
|
171
|
-
|
|
172
|
-
See the [Callbacks](https://github.com/drexed/cmdx/blob/main/docs/callbacks.md#declarations) docs for task level configurations.
|
|
173
|
-
|
|
174
|
-
```ruby
|
|
175
|
-
CMDx.configure do |config|
|
|
176
|
-
# Via method
|
|
177
|
-
config.callbacks.register :before_execution, :initialize_user_session
|
|
178
|
-
|
|
179
|
-
# Via callable (must respond to `call(task)`)
|
|
180
|
-
config.callbacks.register :on_success, LogUserActivity
|
|
181
|
-
|
|
182
|
-
# Via proc or lambda
|
|
183
|
-
config.callbacks.register :on_complete, proc { |task|
|
|
184
|
-
execution_time = task.metadata[:runtime]
|
|
185
|
-
Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"])
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
# With options
|
|
189
|
-
config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task?
|
|
190
|
-
|
|
191
|
-
# Remove callback
|
|
192
|
-
config.callbacks.deregister :on_success, LogUserActivity
|
|
193
|
-
end
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Coercions
|
|
197
|
-
|
|
198
|
-
See the [Attributes - Coercions](https://github.com/drexed/cmdx/blob/main/docs/attributes/coercions.md#declarations) docs for task level configurations.
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
CMDx.configure do |config|
|
|
202
|
-
# Via callable (must respond to `call(value, options)`)
|
|
203
|
-
config.coercions.register :currency, CurrencyCoercion
|
|
204
|
-
|
|
205
|
-
# Via method (must match signature `def coordinates_coercion(value, options)`)
|
|
206
|
-
config.coercions.register :coordinates, :coordinates_coercion
|
|
207
|
-
|
|
208
|
-
# Via proc or lambda
|
|
209
|
-
config.coercions.register :tag_list, proc { |value, options|
|
|
210
|
-
delimiter = options[:delimiter] || ','
|
|
211
|
-
max_tags = options[:max_tags] || 50
|
|
212
|
-
|
|
213
|
-
tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?)
|
|
214
|
-
tags.first(max_tags)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
# Remove coercion
|
|
218
|
-
config.coercions.deregister :currency
|
|
219
|
-
end
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Validators
|
|
223
|
-
|
|
224
|
-
See the [Attributes - Validations](https://github.com/drexed/cmdx/blob/main/docs/attributes/validations.md#declarations) docs for task level configurations.
|
|
225
|
-
|
|
226
|
-
```ruby
|
|
227
|
-
CMDx.configure do |config|
|
|
228
|
-
# Via callable (must respond to `call(value, options)`)
|
|
229
|
-
config.validators.register :username, UsernameValidator
|
|
230
|
-
|
|
231
|
-
# Via method (must match signature `def url_validator(value, options)`)
|
|
232
|
-
config.validators.register :url, :url_validator
|
|
233
|
-
|
|
234
|
-
# Via proc or lambda
|
|
235
|
-
config.validators.register :access_token, proc { |value, options|
|
|
236
|
-
expected_prefix = options[:prefix] || "tok_"
|
|
237
|
-
minimum_length = options[:min_length] || 40
|
|
238
|
-
|
|
239
|
-
value.start_with?(expected_prefix) && value.length >= minimum_length
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# Remove validator
|
|
243
|
-
config.validators.deregister :username
|
|
244
|
-
end
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## Task Configuration
|
|
248
|
-
|
|
249
|
-
### Settings
|
|
250
|
-
|
|
251
|
-
Override global configuration for specific tasks using `settings`:
|
|
252
|
-
|
|
253
|
-
```ruby
|
|
254
|
-
class GenerateInvoice < CMDx::Task
|
|
255
|
-
settings(
|
|
256
|
-
# Global configuration overrides
|
|
257
|
-
task_breakpoints: ["failed"], # Breakpoint override
|
|
258
|
-
workflow_breakpoints: [], # Breakpoint override
|
|
259
|
-
backtrace: true, # Toggle backtrace
|
|
260
|
-
backtrace_cleaner: ->(bt) { bt[0..5] }, # Backtrace cleaner
|
|
261
|
-
logger: CustomLogger.new($stdout), # Custom logger
|
|
262
|
-
|
|
263
|
-
# Task configuration settings
|
|
264
|
-
breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints
|
|
265
|
-
log_level: :info, # Log level override
|
|
266
|
-
log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
|
|
267
|
-
tags: ["billing", "financial"], # Logging tags
|
|
268
|
-
deprecated: true, # Task deprecations
|
|
269
|
-
retries: 3, # Non-fault exception retries
|
|
270
|
-
retry_on: [External::ApiError], # List of exceptions to retry on
|
|
271
|
-
retry_jitter: 1, # Space between retry iteration, eg: current retry num + 1
|
|
272
|
-
rollback_on: ["failed", "skipped"], # Rollback on override
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
def work
|
|
276
|
-
# Your logic here...
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
!!! warning "Important"
|
|
282
|
-
|
|
283
|
-
Retries reuse the same context. By default, all `StandardError` exceptions (including faults) are retried unless you specify `retry_on` option for specific matches.
|
|
284
|
-
|
|
285
|
-
### Registrations
|
|
286
|
-
|
|
287
|
-
Register or deregister middlewares, callbacks, coercions, and validators for specific tasks:
|
|
288
|
-
|
|
289
|
-
```ruby
|
|
290
|
-
class SendCampaignEmail < CMDx::Task
|
|
291
|
-
# Middlewares
|
|
292
|
-
register :middleware, CMDx::Middlewares::Timeout
|
|
293
|
-
deregister :middleware, AuditTrailMiddleware
|
|
294
|
-
|
|
295
|
-
# Callbacks
|
|
296
|
-
register :callback, :on_complete, proc { |task|
|
|
297
|
-
runtime = task.metadata[:runtime]
|
|
298
|
-
Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"])
|
|
299
|
-
}
|
|
300
|
-
deregister :callback, :before_execution, :initialize_user_session
|
|
301
|
-
|
|
302
|
-
# Coercions
|
|
303
|
-
register :coercion, :currency, CurrencyCoercion
|
|
304
|
-
deregister :coercion, :coordinates
|
|
305
|
-
|
|
306
|
-
# Validators
|
|
307
|
-
register :validator, :username, :username_validator
|
|
308
|
-
deregister :validator, :url
|
|
309
|
-
|
|
310
|
-
def work
|
|
311
|
-
# Your logic here...
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
## Configuration Management
|
|
317
|
-
|
|
318
|
-
### Access
|
|
319
|
-
|
|
320
|
-
```ruby
|
|
321
|
-
# Global configuration access
|
|
322
|
-
CMDx.configuration.logger #=> <Logger instance>
|
|
323
|
-
CMDx.configuration.task_breakpoints #=> ["failed"]
|
|
324
|
-
CMDx.configuration.middlewares.registry #=> [<Middleware>, ...]
|
|
325
|
-
|
|
326
|
-
# Task configuration access
|
|
327
|
-
class ProcessUpload < CMDx::Task
|
|
328
|
-
settings(tags: ["files", "storage"])
|
|
329
|
-
|
|
330
|
-
def work
|
|
331
|
-
self.class.settings[:logger] #=> Global configuration value
|
|
332
|
-
self.class.settings[:tags] #=> Task configuration value => ["files", "storage"]
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Resetting
|
|
338
|
-
|
|
339
|
-
!!! warning
|
|
340
|
-
|
|
341
|
-
Resetting affects your entire application. Use this primarily in test environments.
|
|
342
|
-
|
|
343
|
-
```ruby
|
|
344
|
-
# Reset to framework defaults
|
|
345
|
-
CMDx.reset_configuration!
|
|
346
|
-
|
|
347
|
-
# Verify reset
|
|
348
|
-
CMDx.configuration.task_breakpoints #=> ["failed"] (default)
|
|
349
|
-
CMDx.configuration.middlewares.registry #=> Empty registry
|
|
350
|
-
|
|
351
|
-
# Commonly used in test setup (RSpec example)
|
|
352
|
-
RSpec.configure do |config|
|
|
353
|
-
config.before(:each) do
|
|
354
|
-
CMDx.reset_configuration!
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
## Task Generator
|
|
360
|
-
|
|
361
|
-
Generate new CMDx tasks quickly using the built-in generator:
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
rails generate cmdx:task ModerateBlogPost
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
This creates a new task file with the basic structure:
|
|
368
|
-
|
|
369
|
-
```ruby
|
|
370
|
-
# app/tasks/moderate_blog_post.rb
|
|
371
|
-
class ModerateBlogPost < CMDx::Task
|
|
372
|
-
def work
|
|
373
|
-
# Your logic here...
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
!!! tip
|
|
379
|
-
|
|
380
|
-
Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
|
|
381
|
-
|
|
382
|
-
## Type safety
|
|
383
|
-
|
|
384
|
-
CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
|
|
385
|
-
|
|
386
|
-
- **Type checking** β Catch type errors before runtime using tools like Steep or TypeProf
|
|
387
|
-
- **Better IDE support** β Enhanced autocomplete, navigation, and inline documentation
|
|
388
|
-
- **Self-documenting code** β Clear method signatures and return types
|
|
389
|
-
- **Refactoring confidence** β Type-aware refactoring reduces bugs
|
|
390
|
-
|
|
391
|
-
# Basics - Setup
|
|
392
|
-
|
|
393
|
-
Tasks are the heart of CMDxβself-contained units of business logic with built-in validation, error handling, and execution tracking.
|
|
394
|
-
|
|
395
|
-
## Structure
|
|
396
|
-
|
|
397
|
-
Tasks need only two things: inherit from `CMDx::Task` and define a `work` method:
|
|
398
|
-
|
|
399
|
-
```ruby
|
|
400
|
-
class ValidateDocument < CMDx::Task
|
|
401
|
-
def work
|
|
402
|
-
# Your logic here...
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Without a `work` method, execution raises `CMDx::UndefinedMethodError`.
|
|
408
|
-
|
|
409
|
-
```ruby
|
|
410
|
-
class IncompleteTask < CMDx::Task
|
|
411
|
-
# No `work` method defined
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
## Rollback
|
|
418
|
-
|
|
419
|
-
Undo any operations linked to the given status, helping to restore a pristine state.
|
|
420
|
-
|
|
421
|
-
```ruby
|
|
422
|
-
class ValidateDocument < CMDx::Task
|
|
423
|
-
def work
|
|
424
|
-
# Your logic here...
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def rollback
|
|
428
|
-
# Your undo logic...
|
|
429
|
-
end
|
|
430
|
-
end
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
## Inheritance
|
|
434
|
-
|
|
435
|
-
Share configuration across tasks using inheritance:
|
|
436
|
-
|
|
437
|
-
```ruby
|
|
438
|
-
class ApplicationTask < CMDx::Task
|
|
439
|
-
register :middleware, SecurityMiddleware
|
|
440
|
-
|
|
441
|
-
before_execution :initialize_request_tracking
|
|
442
|
-
|
|
443
|
-
attribute :session_id
|
|
444
|
-
|
|
445
|
-
private
|
|
446
|
-
|
|
447
|
-
def initialize_request_tracking
|
|
448
|
-
context.tracking_id ||= SecureRandom.uuid
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
class SyncInventory < ApplicationTask
|
|
453
|
-
def work
|
|
454
|
-
# Your logic here...
|
|
455
|
-
end
|
|
456
|
-
end
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
## Lifecycle
|
|
460
|
-
|
|
461
|
-
Tasks follow a predictable execution pattern:
|
|
462
|
-
|
|
463
|
-
!!! danger "Caution"
|
|
464
|
-
|
|
465
|
-
Tasks are single-use objects. Once executed, they're frozen and immutable.
|
|
466
|
-
|
|
467
|
-
| Stage | State | Status | Description |
|
|
468
|
-
|-------|-------|--------|-------------|
|
|
469
|
-
| **Instantiation** | `initialized` | `success` | Task created with context |
|
|
470
|
-
| **Validation** | `executing` | `success`/`failed` | Attributes validated |
|
|
471
|
-
| **Execution** | `executing` | `success`/`failed`/`skipped` | `work` method runs |
|
|
472
|
-
| **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized |
|
|
473
|
-
| **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable |
|
|
474
|
-
| **Rollback** | `executed` | `failed`/`skipped` | Work undone |
|
|
475
|
-
|
|
476
|
-
# Basics - Execution
|
|
477
|
-
|
|
478
|
-
CMDx offers two execution methods with different error handling approaches. Choose based on your needs: safe result handling or exception-based control flow.
|
|
479
|
-
|
|
480
|
-
## Execution Methods
|
|
481
|
-
|
|
482
|
-
Both methods return results, but handle failures differently:
|
|
483
|
-
|
|
484
|
-
| Method | Returns | Exceptions | Use Case |
|
|
485
|
-
|--------|---------|------------|----------|
|
|
486
|
-
| `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
|
|
487
|
-
| `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
|
|
488
|
-
|
|
489
|
-
## Non-bang Execution
|
|
490
|
-
|
|
491
|
-
Always returns a `CMDx::Result`, never raises exceptions. Perfect for most use cases.
|
|
492
|
-
|
|
493
|
-
```ruby
|
|
494
|
-
result = CreateAccount.execute(email: "user@example.com")
|
|
495
|
-
|
|
496
|
-
# Check execution state
|
|
497
|
-
result.success? #=> true/false
|
|
498
|
-
result.failed? #=> true/false
|
|
499
|
-
result.skipped? #=> true/false
|
|
500
|
-
|
|
501
|
-
# Access result data
|
|
502
|
-
result.context.email #=> "user@example.com"
|
|
503
|
-
result.state #=> "complete"
|
|
504
|
-
result.status #=> "success"
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
## Bang Execution
|
|
508
|
-
|
|
509
|
-
Raises `CMDx::Fault` exceptions on failure or skip. Returns results only on success.
|
|
510
|
-
|
|
511
|
-
| Exception | Raised When |
|
|
512
|
-
|-----------|-------------|
|
|
513
|
-
| `CMDx::FailFault` | Task execution fails |
|
|
514
|
-
| `CMDx::SkipFault` | Task execution is skipped |
|
|
515
|
-
|
|
516
|
-
!!! warning "Important"
|
|
517
|
-
|
|
518
|
-
Behavior depends on `task_breakpoints` or `workflow_breakpoints` config. Default: only failures raise exceptions.
|
|
519
|
-
|
|
520
|
-
```ruby
|
|
521
|
-
begin
|
|
522
|
-
result = CreateAccount.execute!(email: "user@example.com")
|
|
523
|
-
SendWelcomeEmail.execute(result.context)
|
|
524
|
-
rescue CMDx::FailFault => e
|
|
525
|
-
ScheduleAccountRetryJob.perform_later(e.result.context.email)
|
|
526
|
-
rescue CMDx::SkipFault => e
|
|
527
|
-
Rails.logger.info("Account creation skipped: #{e.result.reason}")
|
|
528
|
-
rescue Exception => e
|
|
529
|
-
ErrorTracker.capture(unhandled_exception: e)
|
|
530
|
-
end
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
## Direct Instantiation
|
|
534
|
-
|
|
535
|
-
Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
|
|
536
|
-
|
|
537
|
-
```ruby
|
|
538
|
-
# Direct instantiation
|
|
539
|
-
task = CreateAccount.new(email: "user@example.com", send_welcome: true)
|
|
540
|
-
|
|
541
|
-
# Access properties before execution
|
|
542
|
-
task.id #=> "abc123..." (unique task ID)
|
|
543
|
-
task.context.email #=> "user@example.com"
|
|
544
|
-
task.context.send_welcome #=> true
|
|
545
|
-
task.result.state #=> "initialized"
|
|
546
|
-
task.result.status #=> "success"
|
|
547
|
-
|
|
548
|
-
# Manual execution
|
|
549
|
-
task.execute
|
|
550
|
-
# or
|
|
551
|
-
task.execute!
|
|
552
|
-
|
|
553
|
-
task.result.success? #=> true/false
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
## Result Details
|
|
557
|
-
|
|
558
|
-
The `Result` object provides comprehensive execution information:
|
|
559
|
-
|
|
560
|
-
```ruby
|
|
561
|
-
result = CreateAccount.execute(email: "user@example.com")
|
|
562
|
-
|
|
563
|
-
# Execution metadata
|
|
564
|
-
result.id #=> "abc123..." (unique execution ID)
|
|
565
|
-
result.task #=> CreateAccount instance (frozen)
|
|
566
|
-
result.chain #=> Task execution chain
|
|
567
|
-
|
|
568
|
-
# Context and metadata
|
|
569
|
-
result.context #=> Context with all task data
|
|
570
|
-
result.metadata #=> Hash with execution metadata
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
# Basics - Context
|
|
574
|
-
|
|
575
|
-
Context is your data container for inputs, intermediate values, and outputs. It makes sharing data between tasks effortless.
|
|
576
|
-
|
|
577
|
-
## Assigning Data
|
|
578
|
-
|
|
579
|
-
Context automatically captures all task inputs, normalizing keys to symbols:
|
|
580
|
-
|
|
581
|
-
```ruby
|
|
582
|
-
# Direct execution
|
|
583
|
-
CalculateShipping.execute(weight: 2.5, destination: "CA")
|
|
584
|
-
|
|
585
|
-
# Instance creation
|
|
586
|
-
CalculateShipping.new(weight: 2.5, "destination" => "CA")
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
!!! warning "Important"
|
|
590
|
-
|
|
591
|
-
String keys convert to symbols automatically. Prefer symbols for consistency.
|
|
592
|
-
|
|
593
|
-
## Accessing Data
|
|
594
|
-
|
|
595
|
-
Access context data using method notation, hash keys, or safe accessors:
|
|
596
|
-
|
|
597
|
-
```ruby
|
|
598
|
-
class CalculateShipping < CMDx::Task
|
|
599
|
-
def work
|
|
600
|
-
# Method style access (preferred)
|
|
601
|
-
weight = context.weight
|
|
602
|
-
destination = context.destination
|
|
603
|
-
|
|
604
|
-
# Hash style access
|
|
605
|
-
service_type = context[:service_type]
|
|
606
|
-
options = context["options"]
|
|
607
|
-
|
|
608
|
-
# Safe access with defaults
|
|
609
|
-
rush_delivery = context.fetch!(:rush_delivery, false)
|
|
610
|
-
carrier = context.dig(:options, :carrier)
|
|
611
|
-
|
|
612
|
-
# Shorter alias
|
|
613
|
-
cost = ctx.weight * ctx.rate_per_pound # ctx aliases context
|
|
614
|
-
end
|
|
615
|
-
end
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
!!! warning "Important"
|
|
619
|
-
|
|
620
|
-
Undefined attributes return `nil` instead of raising errorsβperfect for optional data.
|
|
621
|
-
|
|
622
|
-
## Modifying Context
|
|
623
|
-
|
|
624
|
-
Context supports dynamic modification during task execution:
|
|
625
|
-
|
|
626
|
-
```ruby
|
|
627
|
-
class CalculateShipping < CMDx::Task
|
|
628
|
-
def work
|
|
629
|
-
# Direct assignment
|
|
630
|
-
context.carrier = Carrier.find_by(code: context.carrier_code)
|
|
631
|
-
context.package = Package.new(weight: context.weight)
|
|
632
|
-
context.calculated_at = Time.now
|
|
633
|
-
|
|
634
|
-
# Hash-style assignment
|
|
635
|
-
context[:status] = "calculating"
|
|
636
|
-
context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}"
|
|
637
|
-
|
|
638
|
-
# Conditional assignment
|
|
639
|
-
context.insurance_included ||= false
|
|
640
|
-
|
|
641
|
-
# Batch updates
|
|
642
|
-
context.merge!(
|
|
643
|
-
status: "completed",
|
|
644
|
-
shipping_cost: calculate_cost,
|
|
645
|
-
estimated_delivery: Time.now + 3.days
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
# Remove sensitive data
|
|
649
|
-
context.delete!(:credit_card_token)
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
private
|
|
653
|
-
|
|
654
|
-
def calculate_cost
|
|
655
|
-
base_rate = context.weight * context.rate_per_pound
|
|
656
|
-
base_rate + (base_rate * context.tax_percentage)
|
|
657
|
-
end
|
|
658
|
-
end
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
!!! tip
|
|
662
|
-
|
|
663
|
-
Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline.
|
|
664
|
-
|
|
665
|
-
## Data Sharing
|
|
666
|
-
|
|
667
|
-
Share context across tasks for seamless data flow:
|
|
668
|
-
|
|
669
|
-
```ruby
|
|
670
|
-
# During execution
|
|
671
|
-
class CalculateShipping < CMDx::Task
|
|
672
|
-
def work
|
|
673
|
-
# Validate shipping data
|
|
674
|
-
validation_result = ValidateAddress.execute(context)
|
|
675
|
-
|
|
676
|
-
# Via context
|
|
677
|
-
CalculateInsurance.execute(context)
|
|
678
|
-
|
|
679
|
-
# Via result
|
|
680
|
-
NotifyShippingCalculated.execute(validation_result)
|
|
681
|
-
|
|
682
|
-
# Context now contains accumulated data from all tasks
|
|
683
|
-
context.address_validated #=> true (from validation)
|
|
684
|
-
context.insurance_calculated #=> true (from insurance)
|
|
685
|
-
context.notification_sent #=> true (from notification)
|
|
686
|
-
end
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
# After execution
|
|
690
|
-
result = CalculateShipping.execute(destination: "New York, NY")
|
|
691
|
-
|
|
692
|
-
CreateShippingLabel.execute(result)
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
# Basics - Chain
|
|
696
|
-
|
|
697
|
-
Chains automatically track related task executions within a thread. Think of them as execution traces that help you understand what happened and in what order.
|
|
698
|
-
|
|
699
|
-
## Management
|
|
700
|
-
|
|
701
|
-
Each thread maintains its own isolated chain using thread-local storage.
|
|
702
|
-
|
|
703
|
-
!!! warning
|
|
704
|
-
|
|
705
|
-
Chains are thread-local. Don't share chain references across threadsβit causes race conditions.
|
|
706
|
-
|
|
707
|
-
```ruby
|
|
708
|
-
# Thread A
|
|
709
|
-
Thread.new do
|
|
710
|
-
result = ImportDataset.execute(file_path: "/data/batch1.csv")
|
|
711
|
-
result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
# Thread B (completely separate chain)
|
|
715
|
-
Thread.new do
|
|
716
|
-
result = ImportDataset.execute(file_path: "/data/batch2.csv")
|
|
717
|
-
result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
# Access current thread's chain
|
|
721
|
-
CMDx::Chain.current #=> Returns current chain or nil
|
|
722
|
-
CMDx::Chain.clear #=> Clears current thread's chain
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
## Links
|
|
726
|
-
|
|
727
|
-
Tasks automatically create or join the current thread's chain:
|
|
728
|
-
|
|
729
|
-
!!! warning "Important"
|
|
730
|
-
|
|
731
|
-
Chain management is automaticβno manual lifecycle handling needed.
|
|
732
|
-
|
|
733
|
-
```ruby
|
|
734
|
-
class ImportDataset < CMDx::Task
|
|
735
|
-
def work
|
|
736
|
-
# First task creates new chain
|
|
737
|
-
result1 = ValidateHeaders.execute(file_path: context.file_path)
|
|
738
|
-
result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
739
|
-
result1.chain.results.size #=> 1
|
|
740
|
-
|
|
741
|
-
# Second task joins existing chain
|
|
742
|
-
result2 = SendNotification.execute(to: "admin@company.com")
|
|
743
|
-
result2.chain.id == result1.chain.id #=> true
|
|
744
|
-
result2.chain.results.size #=> 2
|
|
745
|
-
|
|
746
|
-
# Both results reference the same chain
|
|
747
|
-
result1.chain.results == result2.chain.results #=> true
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
## Inheritance
|
|
753
|
-
|
|
754
|
-
Subtasks automatically inherit the current thread's chain, building a unified execution trail:
|
|
755
|
-
|
|
756
|
-
```ruby
|
|
757
|
-
class ImportDataset < CMDx::Task
|
|
758
|
-
def work
|
|
759
|
-
context.dataset = Dataset.find(context.dataset_id)
|
|
760
|
-
|
|
761
|
-
# Subtasks automatically inherit current chain
|
|
762
|
-
ValidateSchema.execute
|
|
763
|
-
TransformData.execute!(context)
|
|
764
|
-
SaveToDatabase.execute(dataset_id: context.dataset_id)
|
|
765
|
-
end
|
|
766
|
-
end
|
|
767
|
-
|
|
768
|
-
result = ImportDataset.execute(dataset_id: 456)
|
|
769
|
-
chain = result.chain
|
|
770
|
-
|
|
771
|
-
# All tasks share the same chain
|
|
772
|
-
chain.results.size #=> 4 (main task + 3 subtasks)
|
|
773
|
-
chain.results.map { |r| r.task.class }
|
|
774
|
-
#=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
## Structure
|
|
778
|
-
|
|
779
|
-
Chains expose comprehensive execution information:
|
|
780
|
-
|
|
781
|
-
!!! warning "Important"
|
|
782
|
-
|
|
783
|
-
Chain state reflects the first (outermost) task result. Subtasks maintain their own states.
|
|
784
|
-
|
|
785
|
-
```ruby
|
|
786
|
-
result = ImportDataset.execute(dataset_id: 456)
|
|
787
|
-
chain = result.chain
|
|
788
|
-
|
|
789
|
-
# Chain identification
|
|
790
|
-
chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
791
|
-
chain.results #=> Array of all results in execution order
|
|
792
|
-
|
|
793
|
-
# State delegation (from first/outer-most result)
|
|
794
|
-
chain.state #=> "complete"
|
|
795
|
-
chain.status #=> "success"
|
|
796
|
-
chain.outcome #=> "success"
|
|
797
|
-
|
|
798
|
-
# Access individual results
|
|
799
|
-
chain.results.each_with_index do |result, index|
|
|
800
|
-
puts "#{index}: #{result.task.class} - #{result.status}"
|
|
801
|
-
end
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
# Interruptions - Halt
|
|
805
|
-
|
|
806
|
-
Stop task execution intentionally using `skip!` or `fail!`. Both methods signal clear intent about why execution stopped.
|
|
807
|
-
|
|
808
|
-
## Skipping
|
|
809
|
-
|
|
810
|
-
Use `skip!` when the task doesn't need to run. It's a no-op, not an error.
|
|
811
|
-
|
|
812
|
-
!!! warning "Important"
|
|
813
|
-
|
|
814
|
-
Skipped tasks are considered "good" outcomesβthey succeeded by doing nothing.
|
|
815
|
-
|
|
816
|
-
```ruby
|
|
817
|
-
class ProcessInventory < CMDx::Task
|
|
818
|
-
def work
|
|
819
|
-
# Without a reason
|
|
820
|
-
skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
|
|
821
|
-
|
|
822
|
-
# With a reason
|
|
823
|
-
skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
|
|
824
|
-
|
|
825
|
-
inventory = Inventory.find(context.inventory_id)
|
|
826
|
-
|
|
827
|
-
if inventory.already_counted?
|
|
828
|
-
skip!("Inventory already counted today")
|
|
829
|
-
else
|
|
830
|
-
inventory.count!
|
|
831
|
-
end
|
|
832
|
-
end
|
|
833
|
-
end
|
|
834
|
-
|
|
835
|
-
result = ProcessInventory.execute(inventory_id: 456)
|
|
836
|
-
|
|
837
|
-
# Executed
|
|
838
|
-
result.status #=> "skipped"
|
|
839
|
-
|
|
840
|
-
# Without a reason
|
|
841
|
-
result.reason #=> "Unspecified"
|
|
842
|
-
|
|
843
|
-
# With a reason
|
|
844
|
-
result.reason #=> "Warehouse closed"
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
## Failing
|
|
848
|
-
|
|
849
|
-
Use `fail!` when the task can't complete successfully. It signals controlled, intentional failure:
|
|
850
|
-
|
|
851
|
-
```ruby
|
|
852
|
-
class ProcessRefund < CMDx::Task
|
|
853
|
-
def work
|
|
854
|
-
# Without a reason
|
|
855
|
-
fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
|
|
856
|
-
|
|
857
|
-
refund = Refund.find(context.refund_id)
|
|
858
|
-
|
|
859
|
-
# With a reason
|
|
860
|
-
if refund.expired?
|
|
861
|
-
fail!("Refund period has expired")
|
|
862
|
-
elsif !refund.amount.positive?
|
|
863
|
-
fail!("Refund amount must be positive")
|
|
864
|
-
else
|
|
865
|
-
refund.process!
|
|
866
|
-
end
|
|
867
|
-
end
|
|
868
|
-
end
|
|
869
|
-
|
|
870
|
-
result = ProcessRefund.execute(refund_id: 789)
|
|
871
|
-
|
|
872
|
-
# Executed
|
|
873
|
-
result.status #=> "failed"
|
|
874
|
-
|
|
875
|
-
# Without a reason
|
|
876
|
-
result.reason #=> "Unspecified"
|
|
877
|
-
|
|
878
|
-
# With a reason
|
|
879
|
-
result.reason #=> "Refund period has expired"
|
|
880
|
-
```
|
|
881
|
-
|
|
882
|
-
## Metadata Enrichment
|
|
883
|
-
|
|
884
|
-
Enrich halt calls with metadata for better debugging and error handling:
|
|
885
|
-
|
|
886
|
-
```ruby
|
|
887
|
-
class ProcessRenewal < CMDx::Task
|
|
888
|
-
def work
|
|
889
|
-
license = License.find(context.license_id)
|
|
890
|
-
|
|
891
|
-
if license.already_renewed?
|
|
892
|
-
# Without metadata
|
|
893
|
-
skip!("License already renewed")
|
|
894
|
-
end
|
|
895
|
-
|
|
896
|
-
unless license.renewal_eligible?
|
|
897
|
-
# With metadata
|
|
898
|
-
fail!(
|
|
899
|
-
"License not eligible for renewal",
|
|
900
|
-
error_code: "LICENSE.NOT_ELIGIBLE",
|
|
901
|
-
retry_after: Time.current + 30.days
|
|
902
|
-
)
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
process_renewal
|
|
906
|
-
end
|
|
907
|
-
end
|
|
908
|
-
|
|
909
|
-
result = ProcessRenewal.execute(license_id: 567)
|
|
910
|
-
|
|
911
|
-
# Without metadata
|
|
912
|
-
result.metadata #=> {}
|
|
913
|
-
|
|
914
|
-
# With metadata
|
|
915
|
-
result.metadata #=> {
|
|
916
|
-
# error_code: "LICENSE.NOT_ELIGIBLE",
|
|
917
|
-
# retry_after: <Time 30 days from now>
|
|
918
|
-
# }
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
## State Transitions
|
|
922
|
-
|
|
923
|
-
Halt methods trigger specific state and status transitions:
|
|
924
|
-
|
|
925
|
-
| Method | State | Status | Outcome |
|
|
926
|
-
|--------|-------|--------|---------|
|
|
927
|
-
| `skip!` | `interrupted` | `skipped` | `good? = true`, `bad? = true` |
|
|
928
|
-
| `fail!` | `interrupted` | `failed` | `good? = false`, `bad? = true` |
|
|
929
|
-
|
|
930
|
-
```ruby
|
|
931
|
-
result = ProcessRenewal.execute(license_id: 567)
|
|
932
|
-
|
|
933
|
-
# State information
|
|
934
|
-
result.state #=> "interrupted"
|
|
935
|
-
result.status #=> "skipped" or "failed"
|
|
936
|
-
result.interrupted? #=> true
|
|
937
|
-
result.complete? #=> false
|
|
938
|
-
|
|
939
|
-
# Outcome categorization
|
|
940
|
-
result.good? #=> true for skipped, false for failed
|
|
941
|
-
result.bad? #=> true for both skipped and failed
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
## Execution Behavior
|
|
945
|
-
|
|
946
|
-
Halt methods behave differently depending on the call method used:
|
|
947
|
-
|
|
948
|
-
### Non-bang execution
|
|
949
|
-
|
|
950
|
-
Returns result object without raising exceptions:
|
|
951
|
-
|
|
952
|
-
```ruby
|
|
953
|
-
result = ProcessRefund.execute(refund_id: 789)
|
|
954
|
-
|
|
955
|
-
case result.status
|
|
956
|
-
when "success"
|
|
957
|
-
puts "Refund processed: $#{result.context.refund.amount}"
|
|
958
|
-
when "skipped"
|
|
959
|
-
puts "Refund skipped: #{result.reason}"
|
|
960
|
-
when "failed"
|
|
961
|
-
puts "Refund failed: #{result.reason}"
|
|
962
|
-
handle_refund_error(result.metadata[:error_code])
|
|
963
|
-
end
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
### Bang execution
|
|
967
|
-
|
|
968
|
-
Raises exceptions for halt conditions based on `task_breakpoints` configuration:
|
|
969
|
-
|
|
970
|
-
```ruby
|
|
971
|
-
begin
|
|
972
|
-
result = ProcessRefund.execute!(refund_id: 789)
|
|
973
|
-
puts "Success: Refund processed"
|
|
974
|
-
rescue CMDx::SkipFault => e
|
|
975
|
-
puts "Skipped: #{e.message}"
|
|
976
|
-
rescue CMDx::FailFault => e
|
|
977
|
-
puts "Failed: #{e.message}"
|
|
978
|
-
handle_refund_failure(e.result.metadata[:error_code])
|
|
979
|
-
end
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
## Best Practices
|
|
983
|
-
|
|
984
|
-
Always provide a reason for better debugging and clearer exception messages:
|
|
985
|
-
|
|
986
|
-
```ruby
|
|
987
|
-
# Good: Clear, specific reason
|
|
988
|
-
skip!("Document processing paused for compliance review")
|
|
989
|
-
fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
|
|
990
|
-
|
|
991
|
-
# Acceptable: Generic, non-specific reason
|
|
992
|
-
skip!("Paused")
|
|
993
|
-
fail!("Unsupported")
|
|
994
|
-
|
|
995
|
-
# Bad: Default, cannot determine reason
|
|
996
|
-
skip! #=> "Unspecified"
|
|
997
|
-
fail! #=> "Unspecified"
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
## Manual Errors
|
|
1001
|
-
|
|
1002
|
-
For rare cases, manually add errors before halting:
|
|
1003
|
-
|
|
1004
|
-
!!! warning "Important"
|
|
1005
|
-
|
|
1006
|
-
Manual errors don't stop executionβyou still need to call `fail!` or `skip!`.
|
|
1007
|
-
|
|
1008
|
-
```ruby
|
|
1009
|
-
class ProcessRenewal < CMDx::Task
|
|
1010
|
-
def work
|
|
1011
|
-
if document.nonrenewable?
|
|
1012
|
-
errors.add(:document, "not renewable")
|
|
1013
|
-
fail!("document could not be renewed")
|
|
1014
|
-
else
|
|
1015
|
-
document.renew!
|
|
1016
|
-
end
|
|
1017
|
-
end
|
|
1018
|
-
end
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
# Interruptions - Faults
|
|
1022
|
-
|
|
1023
|
-
Faults are exceptions raised by `execute!` when tasks halt. They carry rich context about execution state, enabling sophisticated error handling patterns.
|
|
1024
|
-
|
|
1025
|
-
## Fault Types
|
|
1026
|
-
|
|
1027
|
-
| Type | Triggered By | Use Case |
|
|
1028
|
-
|------|--------------|----------|
|
|
1029
|
-
| `CMDx::Fault` | Base class | Catch-all for any interruption |
|
|
1030
|
-
| `CMDx::SkipFault` | `skip!` method | Optional processing, early returns |
|
|
1031
|
-
| `CMDx::FailFault` | `fail!` method | Validation errors, processing failures |
|
|
1032
|
-
|
|
1033
|
-
!!! warning "Important"
|
|
1034
|
-
|
|
1035
|
-
All faults inherit from `CMDx::Fault` and expose result, task, context, and chain data.
|
|
1036
|
-
|
|
1037
|
-
## Fault Handling
|
|
1038
|
-
|
|
1039
|
-
```ruby
|
|
1040
|
-
begin
|
|
1041
|
-
ProcessTicket.execute!(ticket_id: 456)
|
|
1042
|
-
rescue CMDx::SkipFault => e
|
|
1043
|
-
logger.info "Ticket processing skipped: #{e.message}"
|
|
1044
|
-
schedule_retry(e.context.ticket_id)
|
|
1045
|
-
rescue CMDx::FailFault => e
|
|
1046
|
-
logger.error "Ticket processing failed: #{e.message}"
|
|
1047
|
-
notify_admin(e.context.assigned_agent, e.result.metadata[:error_code])
|
|
1048
|
-
rescue CMDx::Fault => e
|
|
1049
|
-
logger.warn "Ticket processing interrupted: #{e.message}"
|
|
1050
|
-
rollback_changes
|
|
1051
|
-
end
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
## Data Access
|
|
1055
|
-
|
|
1056
|
-
Access rich execution data from fault exceptions:
|
|
1057
|
-
|
|
1058
|
-
```ruby
|
|
1059
|
-
begin
|
|
1060
|
-
LicenseActivation.execute!(license_key: key, machine_id: machine)
|
|
1061
|
-
rescue CMDx::Fault => e
|
|
1062
|
-
# Result information
|
|
1063
|
-
e.result.state #=> "interrupted"
|
|
1064
|
-
e.result.status #=> "failed" or "skipped"
|
|
1065
|
-
e.result.reason #=> "License key already activated"
|
|
1066
|
-
|
|
1067
|
-
# Task information
|
|
1068
|
-
e.task.class #=> <LicenseActivation>
|
|
1069
|
-
e.task.id #=> "abc123..."
|
|
1070
|
-
|
|
1071
|
-
# Context data
|
|
1072
|
-
e.context.license_key #=> "ABC-123-DEF"
|
|
1073
|
-
e.context.machine_id #=> "[FILTERED]"
|
|
1074
|
-
|
|
1075
|
-
# Chain information
|
|
1076
|
-
e.chain.id #=> "def456..."
|
|
1077
|
-
e.chain.size #=> 3
|
|
1078
|
-
end
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
## Advanced Matching
|
|
1082
|
-
|
|
1083
|
-
### Task-Specific Matching
|
|
1084
|
-
|
|
1085
|
-
Handle faults only from specific tasks using `for?`:
|
|
1086
|
-
|
|
1087
|
-
```ruby
|
|
1088
|
-
begin
|
|
1089
|
-
DocumentWorkflow.execute!(document_data: data)
|
|
1090
|
-
rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e
|
|
1091
|
-
# Handle only document-related failures
|
|
1092
|
-
retry_with_alternate_parser(e.context)
|
|
1093
|
-
rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e
|
|
1094
|
-
# Handle security-related skips
|
|
1095
|
-
quarantine_for_review(e.context.document_id)
|
|
1096
|
-
end
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
### Custom Logic Matching
|
|
1100
|
-
|
|
1101
|
-
```ruby
|
|
1102
|
-
begin
|
|
1103
|
-
ReportGenerator.execute!(report: report_data)
|
|
1104
|
-
rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e
|
|
1105
|
-
escalate_large_dataset_failure(e)
|
|
1106
|
-
rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e
|
|
1107
|
-
abandon_report_generation(e)
|
|
1108
|
-
rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
|
|
1109
|
-
increase_memory_and_retry(e)
|
|
1110
|
-
end
|
|
1111
|
-
```
|
|
1112
|
-
|
|
1113
|
-
## Fault Propagation
|
|
1114
|
-
|
|
1115
|
-
Propagate failures with `throw!` to preserve context and maintain the error chain:
|
|
1116
|
-
|
|
1117
|
-
### Basic Propagation
|
|
1118
|
-
|
|
1119
|
-
```ruby
|
|
1120
|
-
class ReportGenerator < CMDx::Task
|
|
1121
|
-
def work
|
|
1122
|
-
# Throw if skipped or failed
|
|
1123
|
-
validation_result = DataValidator.execute(context)
|
|
1124
|
-
throw!(validation_result)
|
|
1125
|
-
|
|
1126
|
-
# Only throw if skipped
|
|
1127
|
-
check_permissions = CheckPermissions.execute(context)
|
|
1128
|
-
throw!(check_permissions) if check_permissions.skipped?
|
|
1129
|
-
|
|
1130
|
-
# Only throw if failed
|
|
1131
|
-
data_result = DataProcessor.execute(context)
|
|
1132
|
-
throw!(data_result) if data_result.failed?
|
|
1133
|
-
|
|
1134
|
-
# Continue processing
|
|
1135
|
-
generate_report
|
|
1136
|
-
end
|
|
1137
|
-
end
|
|
1138
|
-
```
|
|
1139
|
-
|
|
1140
|
-
### Additional Metadata
|
|
1141
|
-
|
|
1142
|
-
```ruby
|
|
1143
|
-
class BatchProcessor < CMDx::Task
|
|
1144
|
-
def work
|
|
1145
|
-
step_result = FileValidation.execute(context)
|
|
1146
|
-
|
|
1147
|
-
if step_result.failed?
|
|
1148
|
-
throw!(step_result, {
|
|
1149
|
-
batch_stage: "validation",
|
|
1150
|
-
can_retry: true,
|
|
1151
|
-
next_step: "file_repair"
|
|
1152
|
-
})
|
|
1153
|
-
end
|
|
1154
|
-
|
|
1155
|
-
continue_batch
|
|
1156
|
-
end
|
|
1157
|
-
end
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
## Chain Analysis
|
|
1161
|
-
|
|
1162
|
-
Trace fault origins and propagation through the execution chain:
|
|
1163
|
-
|
|
1164
|
-
```ruby
|
|
1165
|
-
result = DocumentWorkflow.execute(invalid_data)
|
|
1166
|
-
|
|
1167
|
-
if result.failed?
|
|
1168
|
-
# Trace the original failure
|
|
1169
|
-
original = result.caused_failure
|
|
1170
|
-
if original
|
|
1171
|
-
puts "Original failure: #{original.task.class.name}"
|
|
1172
|
-
puts "Reason: #{original.reason}"
|
|
1173
|
-
end
|
|
1174
|
-
|
|
1175
|
-
# Find what propagated the failure
|
|
1176
|
-
thrower = result.threw_failure
|
|
1177
|
-
puts "Propagated by: #{thrower.task.class.name}" if thrower
|
|
1178
|
-
|
|
1179
|
-
# Analyze failure type
|
|
1180
|
-
case
|
|
1181
|
-
when result.caused_failure?
|
|
1182
|
-
puts "This task was the original source"
|
|
1183
|
-
when result.threw_failure?
|
|
1184
|
-
puts "This task propagated a failure"
|
|
1185
|
-
when result.thrown_failure?
|
|
1186
|
-
puts "This task failed due to propagation"
|
|
1187
|
-
end
|
|
1188
|
-
end
|
|
1189
|
-
```
|
|
1190
|
-
|
|
1191
|
-
# Interruptions - Exceptions
|
|
1192
|
-
|
|
1193
|
-
Exception handling differs between `execute` and `execute!`. Choose the method that matches your error handling strategy.
|
|
1194
|
-
|
|
1195
|
-
## Exception Handling
|
|
1196
|
-
|
|
1197
|
-
!!! warning "Important"
|
|
1198
|
-
|
|
1199
|
-
Prefer `skip!` and `fail!` over raising exceptionsβthey signal intent more clearly.
|
|
1200
|
-
|
|
1201
|
-
### Non-bang execution
|
|
1202
|
-
|
|
1203
|
-
Captures all exceptions and returns them as failed results:
|
|
1204
|
-
|
|
1205
|
-
```ruby
|
|
1206
|
-
class CompressDocument < CMDx::Task
|
|
1207
|
-
def work
|
|
1208
|
-
document = Document.find(context.document_id)
|
|
1209
|
-
document.compress!
|
|
1210
|
-
end
|
|
1211
|
-
end
|
|
1212
|
-
|
|
1213
|
-
result = CompressDocument.execute(document_id: "unknown-doc-id")
|
|
1214
|
-
result.state #=> "interrupted"
|
|
1215
|
-
result.status #=> "failed"
|
|
1216
|
-
result.failed? #=> true
|
|
1217
|
-
result.reason #=> "[ActiveRecord::NotFoundError] record not found"
|
|
1218
|
-
result.cause #=> <ActiveRecord::NotFoundError>
|
|
1219
|
-
```
|
|
1220
|
-
|
|
1221
|
-
!!! note
|
|
1222
|
-
|
|
1223
|
-
Use `exception_handler` with `execute` to send exceptions to APM tools before they become failed results.
|
|
1224
|
-
|
|
1225
|
-
### Bang execution
|
|
1226
|
-
|
|
1227
|
-
Lets exceptions propagate naturally for standard Ruby error handling:
|
|
1228
|
-
|
|
1229
|
-
```ruby
|
|
1230
|
-
class CompressDocument < CMDx::Task
|
|
1231
|
-
def work
|
|
1232
|
-
document = Document.find(context.document_id)
|
|
1233
|
-
document.compress!
|
|
1234
|
-
end
|
|
1235
|
-
end
|
|
1236
|
-
|
|
1237
|
-
begin
|
|
1238
|
-
CompressDocument.execute!(document_id: "unknown-doc-id")
|
|
1239
|
-
rescue ActiveRecord::NotFoundError => e
|
|
1240
|
-
puts "Handle exception: #{e.message}"
|
|
1241
|
-
end
|
|
1242
|
-
```
|
|
1243
|
-
|
|
1244
|
-
# Outcomes - Result
|
|
1245
|
-
|
|
1246
|
-
Results are your window into task execution. They expose everything: outcome, state, timing, context, and metadata.
|
|
1247
|
-
|
|
1248
|
-
## Result Attributes
|
|
1249
|
-
|
|
1250
|
-
Access essential execution information:
|
|
1251
|
-
|
|
1252
|
-
!!! warning "Important"
|
|
1253
|
-
|
|
1254
|
-
Results are immutable after execution completes.
|
|
1255
|
-
|
|
1256
|
-
```ruby
|
|
1257
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1258
|
-
|
|
1259
|
-
# Object data
|
|
1260
|
-
result.task #=> <BuildApplication>
|
|
1261
|
-
result.context #=> <CMDx::Context>
|
|
1262
|
-
result.chain #=> <CMDx::Chain>
|
|
1263
|
-
|
|
1264
|
-
# Execution data
|
|
1265
|
-
result.state #=> "interrupted"
|
|
1266
|
-
result.status #=> "failed"
|
|
1267
|
-
|
|
1268
|
-
# Fault data
|
|
1269
|
-
result.reason #=> "Build tool not found"
|
|
1270
|
-
result.cause #=> <CMDx::FailFault>
|
|
1271
|
-
result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
## Lifecycle Information
|
|
1275
|
-
|
|
1276
|
-
Check execution state and status with predicate methods:
|
|
1277
|
-
|
|
1278
|
-
```ruby
|
|
1279
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1280
|
-
|
|
1281
|
-
# State predicates (execution lifecycle)
|
|
1282
|
-
result.complete? #=> true (successful completion)
|
|
1283
|
-
result.interrupted? #=> false (no interruption)
|
|
1284
|
-
result.executed? #=> true (execution finished)
|
|
1285
|
-
|
|
1286
|
-
# Status predicates (execution outcome)
|
|
1287
|
-
result.success? #=> true (successful execution)
|
|
1288
|
-
result.failed? #=> false (no failure)
|
|
1289
|
-
result.skipped? #=> false (not skipped)
|
|
1290
|
-
|
|
1291
|
-
# Outcome categorization
|
|
1292
|
-
result.good? #=> true (success or skipped)
|
|
1293
|
-
result.bad? #=> false (skipped or failed)
|
|
1294
|
-
```
|
|
1295
|
-
|
|
1296
|
-
## Outcome Analysis
|
|
1297
|
-
|
|
1298
|
-
Get a unified outcome string combining state and status:
|
|
1299
|
-
|
|
1300
|
-
```ruby
|
|
1301
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1302
|
-
|
|
1303
|
-
result.outcome #=> "success" (state and status)
|
|
1304
|
-
```
|
|
1305
|
-
|
|
1306
|
-
## Chain Analysis
|
|
1307
|
-
|
|
1308
|
-
Trace fault origins and propagation:
|
|
1309
|
-
|
|
1310
|
-
```ruby
|
|
1311
|
-
result = DeploymentWorkflow.execute(app_name: "webapp")
|
|
1312
|
-
|
|
1313
|
-
if result.failed?
|
|
1314
|
-
# Find the original cause of failure
|
|
1315
|
-
if original_failure = result.caused_failure
|
|
1316
|
-
puts "Root cause: #{original_failure.task.class.name}"
|
|
1317
|
-
puts "Reason: #{original_failure.reason}"
|
|
1318
|
-
end
|
|
1319
|
-
|
|
1320
|
-
# Find what threw the failure to this result
|
|
1321
|
-
if throwing_task = result.threw_failure
|
|
1322
|
-
puts "Failure source: #{throwing_task.task.class.name}"
|
|
1323
|
-
puts "Reason: #{throwing_task.reason}"
|
|
1324
|
-
end
|
|
1325
|
-
|
|
1326
|
-
# Failure classification
|
|
1327
|
-
result.caused_failure? #=> true if this result was the original cause
|
|
1328
|
-
result.threw_failure? #=> true if this result threw a failure
|
|
1329
|
-
result.thrown_failure? #=> true if this result received a thrown failure
|
|
1330
|
-
end
|
|
1331
|
-
```
|
|
1332
|
-
|
|
1333
|
-
## Index and Position
|
|
1334
|
-
|
|
1335
|
-
Results track their position within execution chains:
|
|
1336
|
-
|
|
1337
|
-
```ruby
|
|
1338
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1339
|
-
|
|
1340
|
-
# Position in execution sequence
|
|
1341
|
-
result.index #=> 0 (first task in chain)
|
|
1342
|
-
|
|
1343
|
-
# Access via chain
|
|
1344
|
-
result.chain.results[result.index] == result #=> true
|
|
1345
|
-
```
|
|
1346
|
-
|
|
1347
|
-
## Block Yield
|
|
1348
|
-
|
|
1349
|
-
Execute code with direct result access:
|
|
1350
|
-
|
|
1351
|
-
```ruby
|
|
1352
|
-
BuildApplication.execute(version: "1.2.3") do |result|
|
|
1353
|
-
if result.success?
|
|
1354
|
-
notify_deployment_ready(result)
|
|
1355
|
-
elsif result.failed?
|
|
1356
|
-
handle_build_failure(result)
|
|
1357
|
-
else
|
|
1358
|
-
log_skip_reason(result)
|
|
1359
|
-
end
|
|
1360
|
-
end
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
## Handlers
|
|
1364
|
-
|
|
1365
|
-
Handle outcomes with functional-style methods. Handlers return the result for chaining:
|
|
1366
|
-
|
|
1367
|
-
```ruby
|
|
1368
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1369
|
-
|
|
1370
|
-
# Status-based handlers
|
|
1371
|
-
result
|
|
1372
|
-
.handle_success { |result| notify_deployment_ready(result) }
|
|
1373
|
-
.handle_failed { |result| handle_build_failure(result) }
|
|
1374
|
-
.handle_skipped { |result| log_skip_reason(result) }
|
|
1375
|
-
|
|
1376
|
-
# State-based handlers
|
|
1377
|
-
result
|
|
1378
|
-
.handle_complete { |result| update_build_status(result) }
|
|
1379
|
-
.handle_interrupted { |result| cleanup_partial_artifacts(result) }
|
|
1380
|
-
|
|
1381
|
-
# Outcome-based handlers
|
|
1382
|
-
result
|
|
1383
|
-
.handle_good { |result| increment_success_counter(result) }
|
|
1384
|
-
.handle_bad { |result| alert_operations_team(result) }
|
|
1385
|
-
```
|
|
1386
|
-
|
|
1387
|
-
## Pattern Matching
|
|
1388
|
-
|
|
1389
|
-
Use Ruby 3.0+ pattern matching for elegant outcome handling:
|
|
1390
|
-
|
|
1391
|
-
!!! warning "Important"
|
|
1392
|
-
|
|
1393
|
-
Pattern matching works with both array and hash deconstruction.
|
|
1394
|
-
|
|
1395
|
-
### Array Pattern
|
|
1396
|
-
|
|
1397
|
-
```ruby
|
|
1398
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1399
|
-
|
|
1400
|
-
case result
|
|
1401
|
-
in ["complete", "success"]
|
|
1402
|
-
redirect_to build_success_page
|
|
1403
|
-
in ["interrupted", "failed"]
|
|
1404
|
-
retry_build_with_backoff(result)
|
|
1405
|
-
in ["interrupted", "skipped"]
|
|
1406
|
-
log_skip_and_continue
|
|
1407
|
-
end
|
|
1408
|
-
```
|
|
1409
|
-
|
|
1410
|
-
### Hash Pattern
|
|
1411
|
-
|
|
1412
|
-
```ruby
|
|
1413
|
-
result = BuildApplication.execute(version: "1.2.3")
|
|
1414
|
-
|
|
1415
|
-
case result
|
|
1416
|
-
in { state: "complete", status: "success" }
|
|
1417
|
-
celebrate_build_success
|
|
1418
|
-
in { status: "failed", metadata: { retryable: true } }
|
|
1419
|
-
schedule_build_retry(result)
|
|
1420
|
-
in { bad: true, metadata: { reason: String => reason } }
|
|
1421
|
-
escalate_build_error("Build failed: #{reason}")
|
|
1422
|
-
end
|
|
1423
|
-
```
|
|
1424
|
-
|
|
1425
|
-
### Pattern Guards
|
|
1426
|
-
|
|
1427
|
-
```ruby
|
|
1428
|
-
case result
|
|
1429
|
-
in { status: "failed", metadata: { attempts: n } } if n < 3
|
|
1430
|
-
retry_build_with_delay(result, n * 2)
|
|
1431
|
-
in { status: "failed", metadata: { attempts: n } } if n >= 3
|
|
1432
|
-
mark_build_permanently_failed(result)
|
|
1433
|
-
in { runtime: time } if time > performance_threshold
|
|
1434
|
-
investigate_build_performance(result)
|
|
1435
|
-
end
|
|
1436
|
-
```
|
|
1437
|
-
|
|
1438
|
-
# Outcomes - States
|
|
1439
|
-
|
|
1440
|
-
States track where a task is in its execution lifecycleβfrom creation through completion or interruption.
|
|
1441
|
-
|
|
1442
|
-
## Definitions
|
|
1443
|
-
|
|
1444
|
-
| State | Description |
|
|
1445
|
-
| ----- | ----------- |
|
|
1446
|
-
| `initialized` | Task created but execution not yet started. Default state for new tasks. |
|
|
1447
|
-
| `executing` | Task is actively running its business logic. Transient state during execution. |
|
|
1448
|
-
| `complete` | Task finished execution successfully without any interruption or halt. |
|
|
1449
|
-
| `interrupted` | Task execution was stopped due to a fault, exception, or explicit halt. |
|
|
1450
|
-
|
|
1451
|
-
State-Status combinations:
|
|
1452
|
-
|
|
1453
|
-
| State | Status | Meaning |
|
|
1454
|
-
| ----- | ------ | ------- |
|
|
1455
|
-
| `initialized` | `success` | Task created, not yet executed |
|
|
1456
|
-
| `executing` | `success` | Task currently running |
|
|
1457
|
-
| `complete` | `success` | Task finished successfully |
|
|
1458
|
-
| `complete` | `skipped` | Task finished by skipping execution |
|
|
1459
|
-
| `interrupted` | `failed` | Task stopped due to failure |
|
|
1460
|
-
| `interrupted` | `skipped` | Task stopped by skip condition |
|
|
1461
|
-
|
|
1462
|
-
## Transitions
|
|
1463
|
-
|
|
1464
|
-
!!! danger "Caution"
|
|
1465
|
-
|
|
1466
|
-
States are managed automaticallyβnever modify them manually.
|
|
1467
|
-
|
|
1468
|
-
```ruby
|
|
1469
|
-
# Valid state transition flow
|
|
1470
|
-
initialized β executing β complete (successful execution)
|
|
1471
|
-
initialized β executing β interrupted (skipped/failed execution)
|
|
1472
|
-
```
|
|
1473
|
-
|
|
1474
|
-
## Predicates
|
|
1475
|
-
|
|
1476
|
-
Use state predicates to check the current execution lifecycle:
|
|
1477
|
-
|
|
1478
|
-
```ruby
|
|
1479
|
-
result = ProcessVideoUpload.execute
|
|
1480
|
-
|
|
1481
|
-
# Individual state checks
|
|
1482
|
-
result.initialized? #=> false (after execution)
|
|
1483
|
-
result.executing? #=> false (after execution)
|
|
1484
|
-
result.complete? #=> true (successful completion)
|
|
1485
|
-
result.interrupted? #=> false (no interruption)
|
|
1486
|
-
|
|
1487
|
-
# State categorization
|
|
1488
|
-
result.executed? #=> true (complete OR interrupted)
|
|
1489
|
-
```
|
|
1490
|
-
|
|
1491
|
-
## Handlers
|
|
1492
|
-
|
|
1493
|
-
Handle lifecycle events with state-based handlers. Use `handle_executed` for cleanup that runs regardless of outcome:
|
|
1494
|
-
|
|
1495
|
-
```ruby
|
|
1496
|
-
result = ProcessVideoUpload.execute
|
|
1497
|
-
|
|
1498
|
-
# Individual state handlers
|
|
1499
|
-
result
|
|
1500
|
-
.handle_complete { |result| send_upload_notification(result) }
|
|
1501
|
-
.handle_interrupted { |result| cleanup_temp_files(result) }
|
|
1502
|
-
.handle_executed { |result| log_upload_metrics(result) }
|
|
1503
|
-
```
|
|
1504
|
-
|
|
1505
|
-
# Outcomes - Statuses
|
|
1506
|
-
|
|
1507
|
-
Statuses represent the business outcomeβdid the task succeed, skip, or fail? This differs from state, which tracks the execution lifecycle.
|
|
1508
|
-
|
|
1509
|
-
## Definitions
|
|
1510
|
-
|
|
1511
|
-
| Status | Description |
|
|
1512
|
-
| ------ | ----------- |
|
|
1513
|
-
| `success` | Task execution completed successfully with expected business outcome. Default status for all tasks. |
|
|
1514
|
-
| `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary. |
|
|
1515
|
-
| `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions. |
|
|
1516
|
-
|
|
1517
|
-
## Transitions
|
|
1518
|
-
|
|
1519
|
-
!!! warning "Important"
|
|
1520
|
-
|
|
1521
|
-
Status transitions are final and unidirectional. Once skipped or failed, tasks can't return to success.
|
|
1522
|
-
|
|
1523
|
-
```ruby
|
|
1524
|
-
# Valid status transitions
|
|
1525
|
-
success β skipped # via skip!
|
|
1526
|
-
success β failed # via fail! or exception
|
|
1527
|
-
|
|
1528
|
-
# Invalid transitions (will raise errors)
|
|
1529
|
-
skipped β success # β Cannot transition
|
|
1530
|
-
skipped β failed # β Cannot transition
|
|
1531
|
-
failed β success # β Cannot transition
|
|
1532
|
-
failed β skipped # β Cannot transition
|
|
1533
|
-
```
|
|
1534
|
-
|
|
1535
|
-
## Predicates
|
|
1536
|
-
|
|
1537
|
-
Use status predicates to check execution outcomes:
|
|
1538
|
-
|
|
1539
|
-
```ruby
|
|
1540
|
-
result = ProcessNotification.execute
|
|
1541
|
-
|
|
1542
|
-
# Individual status checks
|
|
1543
|
-
result.success? #=> true/false
|
|
1544
|
-
result.skipped? #=> true/false
|
|
1545
|
-
result.failed? #=> true/false
|
|
1546
|
-
|
|
1547
|
-
# Outcome categorization
|
|
1548
|
-
result.good? #=> true if success OR skipped
|
|
1549
|
-
result.bad? #=> true if skipped OR failed (not success)
|
|
1550
|
-
```
|
|
1551
|
-
|
|
1552
|
-
## Handlers
|
|
1553
|
-
|
|
1554
|
-
Branch business logic with status-based handlers. Use `handle_good` and `handle_bad` for success/skip vs failed outcomes:
|
|
1555
|
-
|
|
1556
|
-
```ruby
|
|
1557
|
-
result = ProcessNotification.execute
|
|
1558
|
-
|
|
1559
|
-
# Individual status handlers
|
|
1560
|
-
result
|
|
1561
|
-
.handle_success { |result| mark_notification_sent(result) }
|
|
1562
|
-
.handle_skipped { |result| log_notification_skipped(result) }
|
|
1563
|
-
.handle_failed { |result| queue_retry_notification(result) }
|
|
1564
|
-
|
|
1565
|
-
# Outcome-based handlers
|
|
1566
|
-
result
|
|
1567
|
-
.handle_good { |result| update_message_stats(result) }
|
|
1568
|
-
.handle_bad { |result| track_delivery_failure(result) }
|
|
1569
|
-
```
|
|
1570
|
-
|
|
1571
|
-
# Attributes - Definitions
|
|
1572
|
-
|
|
1573
|
-
Attributes define your task's interface with automatic validation, type coercion, and accessor generation. They're the contract between callers and your business logic.
|
|
1574
|
-
|
|
1575
|
-
## Declarations
|
|
1576
|
-
|
|
1577
|
-
!!! tip
|
|
1578
|
-
|
|
1579
|
-
Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
|
|
1580
|
-
|
|
1581
|
-
### Optional
|
|
1582
|
-
|
|
1583
|
-
Optional attributes return `nil` when not provided.
|
|
1584
|
-
|
|
1585
|
-
```ruby
|
|
1586
|
-
class ScheduleEvent < CMDx::Task
|
|
1587
|
-
attribute :title
|
|
1588
|
-
attributes :duration, :location
|
|
1589
|
-
|
|
1590
|
-
# Alias for attributes (preferred)
|
|
1591
|
-
optional :description
|
|
1592
|
-
optional :visibility, :attendees
|
|
1593
|
-
|
|
1594
|
-
def work
|
|
1595
|
-
title #=> "Team Standup"
|
|
1596
|
-
duration #=> 30
|
|
1597
|
-
location #=> nil
|
|
1598
|
-
description #=> nil
|
|
1599
|
-
visibility #=> nil
|
|
1600
|
-
attendees #=> ["alice@company.com", "bob@company.com"]
|
|
1601
|
-
end
|
|
1602
|
-
end
|
|
1603
|
-
|
|
1604
|
-
# Attributes passed as keyword arguments
|
|
1605
|
-
ScheduleEvent.execute(
|
|
1606
|
-
title: "Team Standup",
|
|
1607
|
-
duration: 30,
|
|
1608
|
-
attendees: ["alice@company.com", "bob@company.com"]
|
|
1609
|
-
)
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
### Required
|
|
1613
|
-
|
|
1614
|
-
Required attributes must be provided in call arguments or task execution will fail.
|
|
1615
|
-
|
|
1616
|
-
```ruby
|
|
1617
|
-
class PublishArticle < CMDx::Task
|
|
1618
|
-
attribute :title, required: true
|
|
1619
|
-
attributes :content, :author_id, required: true
|
|
1620
|
-
|
|
1621
|
-
# Alias for attributes => required: true (preferred)
|
|
1622
|
-
required :category
|
|
1623
|
-
required :status, :tags
|
|
1624
|
-
|
|
1625
|
-
def work
|
|
1626
|
-
title #=> "Getting Started with Ruby"
|
|
1627
|
-
content #=> "This is a comprehensive guide..."
|
|
1628
|
-
author_id #=> 42
|
|
1629
|
-
category #=> "programming"
|
|
1630
|
-
status #=> :published
|
|
1631
|
-
tags #=> ["ruby", "beginner"]
|
|
1632
|
-
end
|
|
1633
|
-
end
|
|
1634
|
-
|
|
1635
|
-
# Attributes passed as keyword arguments
|
|
1636
|
-
PublishArticle.execute(
|
|
1637
|
-
title: "Getting Started with Ruby",
|
|
1638
|
-
content: "This is a comprehensive guide...",
|
|
1639
|
-
author_id: 42,
|
|
1640
|
-
category: "programming",
|
|
1641
|
-
status: :published,
|
|
1642
|
-
tags: ["ruby", "beginner"]
|
|
1643
|
-
)
|
|
1644
|
-
```
|
|
1645
|
-
|
|
1646
|
-
## Sources
|
|
1647
|
-
|
|
1648
|
-
Attributes read from any accessible objectβnot just context. Use sources to pull data from models, services, or any callable:
|
|
1649
|
-
|
|
1650
|
-
### Context
|
|
1651
|
-
|
|
1652
|
-
```ruby
|
|
1653
|
-
class BackupDatabase < CMDx::Task
|
|
1654
|
-
# Default source is :context
|
|
1655
|
-
required :database_name
|
|
1656
|
-
optional :compression_level
|
|
1657
|
-
|
|
1658
|
-
# Explicitly specify context source
|
|
1659
|
-
attribute :backup_path, source: :context
|
|
1660
|
-
|
|
1661
|
-
def work
|
|
1662
|
-
database_name #=> context.database_name
|
|
1663
|
-
backup_path #=> context.backup_path
|
|
1664
|
-
compression_level #=> context.compression_level
|
|
1665
|
-
end
|
|
1666
|
-
end
|
|
1667
|
-
```
|
|
1668
|
-
|
|
1669
|
-
### Symbol References
|
|
1670
|
-
|
|
1671
|
-
Reference instance methods by symbol for dynamic source values:
|
|
1672
|
-
|
|
1673
|
-
```ruby
|
|
1674
|
-
class BackupDatabase < CMDx::Task
|
|
1675
|
-
attributes :host, :credentials, source: :database_config
|
|
1676
|
-
|
|
1677
|
-
# Access from declared attributes
|
|
1678
|
-
attribute :connection_string, source: :credentials
|
|
1679
|
-
|
|
1680
|
-
def work
|
|
1681
|
-
# Your logic here...
|
|
1682
|
-
end
|
|
1683
|
-
|
|
1684
|
-
private
|
|
1685
|
-
|
|
1686
|
-
def database_config
|
|
1687
|
-
@database_config ||= DatabaseConfig.find(context.database_name)
|
|
1688
|
-
end
|
|
1689
|
-
end
|
|
1690
|
-
```
|
|
1691
|
-
|
|
1692
|
-
### Proc or Lambda
|
|
1693
|
-
|
|
1694
|
-
Use anonymous functions for dynamic source values:
|
|
1695
|
-
|
|
1696
|
-
```ruby
|
|
1697
|
-
class BackupDatabase < CMDx::Task
|
|
1698
|
-
# Proc
|
|
1699
|
-
attribute :timestamp, source: proc { Time.current }
|
|
1700
|
-
|
|
1701
|
-
# Lambda
|
|
1702
|
-
attribute :server, source: -> { Current.server }
|
|
1703
|
-
end
|
|
1704
|
-
```
|
|
1705
|
-
|
|
1706
|
-
### Class or Module
|
|
1707
|
-
|
|
1708
|
-
For complex source logic, use classes or modules:
|
|
1709
|
-
|
|
1710
|
-
```ruby
|
|
1711
|
-
class DatabaseResolver
|
|
1712
|
-
def self.call(task)
|
|
1713
|
-
Database.find(task.context.database_name)
|
|
1714
|
-
end
|
|
1715
|
-
end
|
|
1716
|
-
|
|
1717
|
-
class BackupDatabase < CMDx::Task
|
|
1718
|
-
# Class or Module
|
|
1719
|
-
attribute :schema, source: DatabaseResolver
|
|
1720
|
-
|
|
1721
|
-
# Instance
|
|
1722
|
-
attribute :metadata, source: DatabaseResolver.new
|
|
1723
|
-
end
|
|
1724
|
-
```
|
|
1725
|
-
|
|
1726
|
-
## Nesting
|
|
1727
|
-
|
|
1728
|
-
Build complex structures with nested attributes. Children inherit their parent as source and support all attribute options:
|
|
1729
|
-
|
|
1730
|
-
!!! note
|
|
1731
|
-
|
|
1732
|
-
Nested attributes support all features: naming, coercions, validations, defaults, and more.
|
|
1733
|
-
|
|
1734
|
-
```ruby
|
|
1735
|
-
class ConfigureServer < CMDx::Task
|
|
1736
|
-
# Required parent with required children
|
|
1737
|
-
required :network_config do
|
|
1738
|
-
required :hostname, :port, :protocol, :subnet
|
|
1739
|
-
optional :load_balancer
|
|
1740
|
-
attribute :firewall_rules
|
|
1741
|
-
end
|
|
1742
|
-
|
|
1743
|
-
# Optional parent with conditional children
|
|
1744
|
-
optional :ssl_config do
|
|
1745
|
-
required :certificate_path, :private_key # Only required if ssl_config provided
|
|
1746
|
-
optional :enable_http2, prefix: true
|
|
1747
|
-
end
|
|
1748
|
-
|
|
1749
|
-
# Multi-level nesting
|
|
1750
|
-
attribute :monitoring do
|
|
1751
|
-
required :provider
|
|
1752
|
-
|
|
1753
|
-
optional :alerting do
|
|
1754
|
-
required :threshold_percentage
|
|
1755
|
-
optional :notification_channel
|
|
1756
|
-
end
|
|
1757
|
-
end
|
|
1758
|
-
|
|
1759
|
-
def work
|
|
1760
|
-
network_config #=> { hostname: "api.company.com" ... }
|
|
1761
|
-
hostname #=> "api.company.com"
|
|
1762
|
-
load_balancer #=> nil
|
|
1763
|
-
end
|
|
1764
|
-
end
|
|
1765
|
-
|
|
1766
|
-
ConfigureServer.execute(
|
|
1767
|
-
server_id: "srv-001",
|
|
1768
|
-
network_config: {
|
|
1769
|
-
hostname: "api.company.com",
|
|
1770
|
-
port: 443,
|
|
1771
|
-
protocol: "https",
|
|
1772
|
-
subnet: "10.0.1.0/24",
|
|
1773
|
-
firewall_rules: "allow_web_traffic"
|
|
1774
|
-
},
|
|
1775
|
-
monitoring: {
|
|
1776
|
-
provider: "datadog",
|
|
1777
|
-
alerting: {
|
|
1778
|
-
threshold_percentage: 85.0,
|
|
1779
|
-
notification_channel: "slack"
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
)
|
|
1783
|
-
```
|
|
1784
|
-
|
|
1785
|
-
!!! warning "Important"
|
|
1786
|
-
|
|
1787
|
-
Child requirements only apply when the parent is providedβperfect for optional structures.
|
|
1788
|
-
|
|
1789
|
-
## Error Handling
|
|
1790
|
-
|
|
1791
|
-
Validation failures provide detailed, structured error messages:
|
|
1792
|
-
|
|
1793
|
-
!!! note
|
|
1794
|
-
|
|
1795
|
-
Nested attributes are only validated when their parent is present and valid.
|
|
1796
|
-
|
|
1797
|
-
```ruby
|
|
1798
|
-
class ConfigureServer < CMDx::Task
|
|
1799
|
-
required :server_id, :environment
|
|
1800
|
-
required :network_config do
|
|
1801
|
-
required :hostname, :port
|
|
1802
|
-
end
|
|
1803
|
-
|
|
1804
|
-
def work
|
|
1805
|
-
# Your logic here...
|
|
1806
|
-
end
|
|
1807
|
-
end
|
|
1808
|
-
|
|
1809
|
-
# Missing required top-level attributes
|
|
1810
|
-
result = ConfigureServer.execute(server_id: "srv-001")
|
|
1811
|
-
|
|
1812
|
-
result.state #=> "interrupted"
|
|
1813
|
-
result.status #=> "failed"
|
|
1814
|
-
result.reason #=> "Invalid"
|
|
1815
|
-
result.metadata #=> {
|
|
1816
|
-
# errors: {
|
|
1817
|
-
# full_message: "environment is required. network_config is required.",
|
|
1818
|
-
# messages: {
|
|
1819
|
-
# environment: ["is required"],
|
|
1820
|
-
# network_config: ["is required"]
|
|
1821
|
-
# }
|
|
1822
|
-
# }
|
|
1823
|
-
# }
|
|
1824
|
-
|
|
1825
|
-
# Missing required nested attributes
|
|
1826
|
-
result = ConfigureServer.execute(
|
|
1827
|
-
server_id: "srv-001",
|
|
1828
|
-
environment: "production",
|
|
1829
|
-
network_config: { hostname: "api.company.com" } # Missing port
|
|
1830
|
-
)
|
|
1831
|
-
|
|
1832
|
-
result.state #=> "interrupted"
|
|
1833
|
-
result.status #=> "failed"
|
|
1834
|
-
result.reason #=> "Invalid"
|
|
1835
|
-
result.metadata #=> {
|
|
1836
|
-
# errors: {
|
|
1837
|
-
# full_message: "port is required.",
|
|
1838
|
-
# messages: {
|
|
1839
|
-
# port: ["is required"]
|
|
1840
|
-
# }
|
|
1841
|
-
# }
|
|
1842
|
-
# }
|
|
1843
|
-
```
|
|
1844
|
-
|
|
1845
|
-
# Attributes - Naming
|
|
1846
|
-
|
|
1847
|
-
Customize accessor method names to avoid conflicts and improve clarity. Affixing changes only the generated methodsβnot the original attribute names.
|
|
1848
|
-
|
|
1849
|
-
!!! note
|
|
1850
|
-
|
|
1851
|
-
Use naming when attributes conflict with existing methods or need better clarity in your code.
|
|
1852
|
-
|
|
1853
|
-
## Prefix
|
|
1854
|
-
|
|
1855
|
-
Adds a prefix to the generated accessor method name.
|
|
1856
|
-
|
|
1857
|
-
```ruby
|
|
1858
|
-
class GenerateReport < CMDx::Task
|
|
1859
|
-
# Dynamic from attribute source
|
|
1860
|
-
attribute :template, prefix: true
|
|
1861
|
-
|
|
1862
|
-
# Static
|
|
1863
|
-
attribute :format, prefix: "report_"
|
|
1864
|
-
|
|
1865
|
-
def work
|
|
1866
|
-
context_template #=> "monthly_sales"
|
|
1867
|
-
report_format #=> "pdf"
|
|
1868
|
-
end
|
|
1869
|
-
end
|
|
1870
|
-
|
|
1871
|
-
# Attributes passed as original attribute names
|
|
1872
|
-
GenerateReport.execute(template: "monthly_sales", format: "pdf")
|
|
1873
|
-
```
|
|
1874
|
-
|
|
1875
|
-
## Suffix
|
|
1876
|
-
|
|
1877
|
-
Adds a suffix to the generated accessor method name.
|
|
1878
|
-
|
|
1879
|
-
```ruby
|
|
1880
|
-
class DeployApplication < CMDx::Task
|
|
1881
|
-
# Dynamic from attribute source
|
|
1882
|
-
attribute :branch, suffix: true
|
|
1883
|
-
|
|
1884
|
-
# Static
|
|
1885
|
-
attribute :version, suffix: "_tag"
|
|
1886
|
-
|
|
1887
|
-
def work
|
|
1888
|
-
branch_context #=> "main"
|
|
1889
|
-
version_tag #=> "v1.2.3"
|
|
1890
|
-
end
|
|
1891
|
-
end
|
|
1892
|
-
|
|
1893
|
-
# Attributes passed as original attribute names
|
|
1894
|
-
DeployApplication.execute(branch: "main", version: "v1.2.3")
|
|
1895
|
-
```
|
|
1896
|
-
|
|
1897
|
-
## As
|
|
1898
|
-
|
|
1899
|
-
Completely renames the generated accessor method.
|
|
1900
|
-
|
|
1901
|
-
```ruby
|
|
1902
|
-
class ScheduleMaintenance < CMDx::Task
|
|
1903
|
-
attribute :scheduled_at, as: :when
|
|
1904
|
-
|
|
1905
|
-
def work
|
|
1906
|
-
when #=> <DateTime>
|
|
1907
|
-
end
|
|
1908
|
-
end
|
|
1909
|
-
|
|
1910
|
-
# Attributes passed as original attribute names
|
|
1911
|
-
ScheduleMaintenance.execute(scheduled_at: DateTime.new(2024, 12, 15, 2, 0, 0))
|
|
1912
|
-
```
|
|
1913
|
-
|
|
1914
|
-
# Attributes - Coercions
|
|
1915
|
-
|
|
1916
|
-
Automatically convert inputs to expected types. Coercions handle everything from simple string-to-integer conversions to JSON parsing.
|
|
1917
|
-
|
|
1918
|
-
See [Global Configuration](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#coercions) for custom coercion setup.
|
|
1919
|
-
|
|
1920
|
-
## Usage
|
|
1921
|
-
|
|
1922
|
-
Define attribute types to enable automatic coercion:
|
|
1923
|
-
|
|
1924
|
-
```ruby
|
|
1925
|
-
class ParseMetrics < CMDx::Task
|
|
1926
|
-
# Coerce into a symbol
|
|
1927
|
-
attribute :measurement_type, type: :symbol
|
|
1928
|
-
|
|
1929
|
-
# Coerce into a rational fallback to big decimal
|
|
1930
|
-
attribute :value, type: [:rational, :big_decimal]
|
|
1931
|
-
|
|
1932
|
-
# Coerce with options
|
|
1933
|
-
attribute :recorded_at, type: :date, strptime: "%m-%d-%Y"
|
|
1934
|
-
|
|
1935
|
-
def work
|
|
1936
|
-
measurement_type #=> :temperature
|
|
1937
|
-
recorded_at #=> <Date 2024-01-23>
|
|
1938
|
-
value #=> 98.6 (Float)
|
|
1939
|
-
end
|
|
1940
|
-
end
|
|
1941
|
-
|
|
1942
|
-
ParseMetrics.execute(
|
|
1943
|
-
measurement_type: "temperature",
|
|
1944
|
-
recorded_at: "01-23-2020",
|
|
1945
|
-
value: "98.6"
|
|
1946
|
-
)
|
|
1947
|
-
```
|
|
1948
|
-
|
|
1949
|
-
!!! tip
|
|
1950
|
-
|
|
1951
|
-
Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds.
|
|
1952
|
-
|
|
1953
|
-
## Built-in Coercions
|
|
1954
|
-
|
|
1955
|
-
| Type | Options | Description | Examples |
|
|
1956
|
-
|------|---------|-------------|----------|
|
|
1957
|
-
| `:array` | | Array conversion with JSON support | `"val"` β `["val"]`<br>`"[1,2,3]"` β `[1, 2, 3]` |
|
|
1958
|
-
| `:big_decimal` | `:precision` | High-precision decimal | `"123.456"` β `BigDecimal("123.456")` |
|
|
1959
|
-
| `:boolean` | | Boolean with text patterns | `"yes"` β `true`, `"no"` β `false` |
|
|
1960
|
-
| `:complex` | | Complex numbers | `"1+2i"` β `Complex(1, 2)` |
|
|
1961
|
-
| `:date` | `:strptime` | Date objects | `"2024-01-23"` β `Date.new(2024, 1, 23)` |
|
|
1962
|
-
| `:datetime` | `:strptime` | DateTime objects | `"2024-01-23 10:30"` β `DateTime.new(2024, 1, 23, 10, 30)` |
|
|
1963
|
-
| `:float` | | Floating-point numbers | `"123.45"` β `123.45` |
|
|
1964
|
-
| `:hash` | | Hash conversion with JSON support | `'{"a":1}'` β `{"a" => 1}` |
|
|
1965
|
-
| `:integer` | | Integer with hex/octal support | `"0xFF"` β `255`, `"077"` β `63` |
|
|
1966
|
-
| `:rational` | | Rational numbers | `"1/2"` β `Rational(1, 2)` |
|
|
1967
|
-
| `:string` | | String conversion | `123` β `"123"` |
|
|
1968
|
-
| `:symbol` | | Symbol conversion | `"abc"` β `:abc` |
|
|
1969
|
-
| `:time` | `:strptime` | Time objects | `"10:30:00"` β `Time.new(2024, 1, 23, 10, 30)` |
|
|
1970
|
-
|
|
1971
|
-
## Declarations
|
|
1972
|
-
|
|
1973
|
-
!!! warning "Important"
|
|
1974
|
-
|
|
1975
|
-
Custom coercions must raise `CMDx::CoercionError` with a descriptive message.
|
|
1976
|
-
|
|
1977
|
-
### Proc or Lambda
|
|
1978
|
-
|
|
1979
|
-
Use anonymous functions for simple coercion logic:
|
|
1980
|
-
|
|
1981
|
-
```ruby
|
|
1982
|
-
class TransformCoordinates < CMDx::Task
|
|
1983
|
-
# Proc
|
|
1984
|
-
register :callback, :geolocation, proc do |value, options = {}|
|
|
1985
|
-
begin
|
|
1986
|
-
Geolocation(value)
|
|
1987
|
-
rescue StandardError
|
|
1988
|
-
raise CMDx::CoercionError, "could not convert into a geolocation"
|
|
1989
|
-
end
|
|
1990
|
-
end
|
|
1991
|
-
|
|
1992
|
-
# Lambda
|
|
1993
|
-
register :callback, :geolocation, ->(value, options = {}) {
|
|
1994
|
-
begin
|
|
1995
|
-
Geolocation(value)
|
|
1996
|
-
rescue StandardError
|
|
1997
|
-
raise CMDx::CoercionError, "could not convert into a geolocation"
|
|
1998
|
-
end
|
|
1999
|
-
}
|
|
2000
|
-
end
|
|
2001
|
-
```
|
|
2002
|
-
|
|
2003
|
-
### Class or Module
|
|
2004
|
-
|
|
2005
|
-
Register custom coercion logic for specialized type handling:
|
|
2006
|
-
|
|
2007
|
-
```ruby
|
|
2008
|
-
class GeolocationCoercion
|
|
2009
|
-
def self.call(value, options = {})
|
|
2010
|
-
Geolocation(value)
|
|
2011
|
-
rescue StandardError
|
|
2012
|
-
raise CMDx::CoercionError, "could not convert into a geolocation"
|
|
2013
|
-
end
|
|
2014
|
-
end
|
|
2015
|
-
|
|
2016
|
-
class TransformCoordinates < CMDx::Task
|
|
2017
|
-
register :coercion, :geolocation, GeolocationCoercion
|
|
2018
|
-
|
|
2019
|
-
attribute :latitude, type: :geolocation
|
|
2020
|
-
end
|
|
2021
|
-
```
|
|
2022
|
-
|
|
2023
|
-
## Removals
|
|
2024
|
-
|
|
2025
|
-
Remove unwanted coercions:
|
|
2026
|
-
|
|
2027
|
-
!!! warning
|
|
2028
|
-
|
|
2029
|
-
Each `deregister` call removes one coercion. Use multiple calls for batch removals.
|
|
2030
|
-
|
|
2031
|
-
```ruby
|
|
2032
|
-
class TransformCoordinates < CMDx::Task
|
|
2033
|
-
deregister :coercion, :geolocation
|
|
2034
|
-
end
|
|
2035
|
-
```
|
|
2036
|
-
|
|
2037
|
-
## Error Handling
|
|
2038
|
-
|
|
2039
|
-
Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons:
|
|
2040
|
-
|
|
2041
|
-
```ruby
|
|
2042
|
-
class AnalyzePerformance < CMDx::Task
|
|
2043
|
-
attribute :iterations, type: :integer
|
|
2044
|
-
attribute :score, type: [:float, :big_decimal]
|
|
2045
|
-
|
|
2046
|
-
def work
|
|
2047
|
-
# Your logic here...
|
|
2048
|
-
end
|
|
2049
|
-
end
|
|
2050
|
-
|
|
2051
|
-
result = AnalyzePerformance.execute(
|
|
2052
|
-
iterations: "not-a-number",
|
|
2053
|
-
score: "invalid-float"
|
|
2054
|
-
)
|
|
2055
|
-
|
|
2056
|
-
result.state #=> "interrupted"
|
|
2057
|
-
result.status #=> "failed"
|
|
2058
|
-
result.reason #=> "Invalid"
|
|
2059
|
-
result.metadata #=> {
|
|
2060
|
-
# errors: {
|
|
2061
|
-
# full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
|
|
2062
|
-
# messages: {
|
|
2063
|
-
# iterations: ["could not coerce into an integer"],
|
|
2064
|
-
# score: ["could not coerce into one of: float, big_decimal"]
|
|
2065
|
-
# }
|
|
2066
|
-
# }
|
|
2067
|
-
# }
|
|
2068
|
-
```
|
|
2069
|
-
|
|
2070
|
-
# Attributes - Validations
|
|
2071
|
-
|
|
2072
|
-
Ensure inputs meet requirements before execution. Validations run after coercions, giving you declarative data integrity checks.
|
|
2073
|
-
|
|
2074
|
-
See [Global Configuration](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#validators) for custom validator setup.
|
|
2075
|
-
|
|
2076
|
-
## Usage
|
|
2077
|
-
|
|
2078
|
-
Define validation rules on attributes to enforce data requirements:
|
|
2079
|
-
|
|
2080
|
-
```ruby
|
|
2081
|
-
class ProcessSubscription < CMDx::Task
|
|
2082
|
-
# Required field with presence validation
|
|
2083
|
-
attribute :user_id, presence: true
|
|
2084
|
-
|
|
2085
|
-
# String with length constraints
|
|
2086
|
-
attribute :preferences, length: { minimum: 10, maximum: 500 }
|
|
2087
|
-
|
|
2088
|
-
# Numeric range validation
|
|
2089
|
-
attribute :tier_level, inclusion: { in: 1..5 }
|
|
2090
|
-
|
|
2091
|
-
# Format validation for email
|
|
2092
|
-
attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
2093
|
-
|
|
2094
|
-
def work
|
|
2095
|
-
user_id #=> "98765"
|
|
2096
|
-
preferences #=> "Send weekly digest emails"
|
|
2097
|
-
tier_level #=> 3
|
|
2098
|
-
contact_email #=> "user@company.com"
|
|
2099
|
-
end
|
|
2100
|
-
end
|
|
2101
|
-
|
|
2102
|
-
ProcessSubscription.execute(
|
|
2103
|
-
user_id: "98765",
|
|
2104
|
-
preferences: "Send weekly digest emails",
|
|
2105
|
-
tier_level: 3,
|
|
2106
|
-
contact_email: "user@company.com"
|
|
2107
|
-
)
|
|
2108
|
-
```
|
|
2109
|
-
|
|
2110
|
-
!!! tip
|
|
2111
|
-
|
|
2112
|
-
Validations run after coercions, so you can validate the final coerced values rather than raw input.
|
|
2113
|
-
|
|
2114
|
-
## Built-in Validators
|
|
2115
|
-
|
|
2116
|
-
### Common Options
|
|
2117
|
-
|
|
2118
|
-
This list of options is available to all validators:
|
|
2119
|
-
|
|
2120
|
-
| Option | Description |
|
|
2121
|
-
|--------|-------------|
|
|
2122
|
-
| `:allow_nil` | Skip validation when value is `nil` |
|
|
2123
|
-
| `:if` | Symbol, proc, lambda, or callable determining when to validate |
|
|
2124
|
-
| `:unless` | Symbol, proc, lambda, or callable determining when to skip validation |
|
|
2125
|
-
| `:message` | Custom error message for validation failures |
|
|
2126
|
-
|
|
2127
|
-
### Exclusion
|
|
2128
|
-
|
|
2129
|
-
```ruby
|
|
2130
|
-
class ProcessProduct < CMDx::Task
|
|
2131
|
-
attribute :status, exclusion: { in: %w[recalled archived] }
|
|
2132
|
-
|
|
2133
|
-
def work
|
|
2134
|
-
# Your logic here...
|
|
2135
|
-
end
|
|
2136
|
-
end
|
|
2137
|
-
```
|
|
2138
|
-
|
|
2139
|
-
| Options | Description |
|
|
2140
|
-
|---------|-------------|
|
|
2141
|
-
| `:in` | The collection of forbidden values or range |
|
|
2142
|
-
| `:within` | Alias for :in option |
|
|
2143
|
-
| `:of_message` | Custom message for discrete value exclusions |
|
|
2144
|
-
| `:in_message` | Custom message for range-based exclusions |
|
|
2145
|
-
| `:within_message` | Alias for :in_message option |
|
|
2146
|
-
|
|
2147
|
-
### Format
|
|
2148
|
-
|
|
2149
|
-
```ruby
|
|
2150
|
-
class ProcessProduct < CMDx::Task
|
|
2151
|
-
attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
|
|
2152
|
-
|
|
2153
|
-
attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
|
|
2154
|
-
|
|
2155
|
-
def work
|
|
2156
|
-
# Your logic here...
|
|
2157
|
-
end
|
|
2158
|
-
end
|
|
2159
|
-
```
|
|
2160
|
-
|
|
2161
|
-
| Options | Description |
|
|
2162
|
-
|---------|-------------|
|
|
2163
|
-
| `regexp` | Alias for :with option |
|
|
2164
|
-
| `:with` | Regex pattern that the value must match |
|
|
2165
|
-
| `:without` | Regex pattern that the value must not match |
|
|
2166
|
-
|
|
2167
|
-
### Inclusion
|
|
2168
|
-
|
|
2169
|
-
```ruby
|
|
2170
|
-
class ProcessProduct < CMDx::Task
|
|
2171
|
-
attribute :availability, inclusion: { in: %w[available limited] }
|
|
2172
|
-
|
|
2173
|
-
def work
|
|
2174
|
-
# Your logic here...
|
|
2175
|
-
end
|
|
2176
|
-
end
|
|
2177
|
-
```
|
|
2178
|
-
|
|
2179
|
-
| Options | Description |
|
|
2180
|
-
|---------|-------------|
|
|
2181
|
-
| `:in` | The collection of allowed values or range |
|
|
2182
|
-
| `:within` | Alias for :in option |
|
|
2183
|
-
| `:of_message` | Custom message for discrete value inclusions |
|
|
2184
|
-
| `:in_message` | Custom message for range-based inclusions |
|
|
2185
|
-
| `:within_message` | Alias for :in_message option |
|
|
2186
|
-
|
|
2187
|
-
### Length
|
|
2188
|
-
|
|
2189
|
-
```ruby
|
|
2190
|
-
class CreateBlogPost < CMDx::Task
|
|
2191
|
-
attribute :title, length: { within: 5..100 }
|
|
2192
|
-
|
|
2193
|
-
def work
|
|
2194
|
-
# Your logic here...
|
|
2195
|
-
end
|
|
2196
|
-
end
|
|
2197
|
-
```
|
|
2198
|
-
|
|
2199
|
-
| Options | Description |
|
|
2200
|
-
|---------|-------------|
|
|
2201
|
-
| `:within` | Range that the length must fall within (inclusive) |
|
|
2202
|
-
| `:not_within` | Range that the length must not fall within |
|
|
2203
|
-
| `:in` | Alias for :within |
|
|
2204
|
-
| `:not_in` | Range that the length must not fall within |
|
|
2205
|
-
| `:min` | Minimum allowed length |
|
|
2206
|
-
| `:max` | Maximum allowed length |
|
|
2207
|
-
| `:is` | Exact required length |
|
|
2208
|
-
| `:is_not` | Length that is not allowed |
|
|
2209
|
-
| `:within_message` | Custom message for within/range validations |
|
|
2210
|
-
| `:in_message` | Custom message for :in validation |
|
|
2211
|
-
| `:not_within_message` | Custom message for not_within validation |
|
|
2212
|
-
| `:not_in_message` | Custom message for not_in validation |
|
|
2213
|
-
| `:min_message` | Custom message for minimum length validation |
|
|
2214
|
-
| `:max_message` | Custom message for maximum length validation |
|
|
2215
|
-
| `:is_message` | Custom message for exact length validation |
|
|
2216
|
-
| `:is_not_message` | Custom message for is_not validation |
|
|
2217
|
-
|
|
2218
|
-
### Numeric
|
|
2219
|
-
|
|
2220
|
-
```ruby
|
|
2221
|
-
class CreateBlogPost < CMDx::Task
|
|
2222
|
-
attribute :word_count, numeric: { min: 100 }
|
|
2223
|
-
|
|
2224
|
-
def work
|
|
2225
|
-
# Your logic here...
|
|
2226
|
-
end
|
|
2227
|
-
end
|
|
2228
|
-
```
|
|
2229
|
-
|
|
2230
|
-
| Options | Description |
|
|
2231
|
-
|---------|-------------|
|
|
2232
|
-
| `:within` | Range that the value must fall within (inclusive) |
|
|
2233
|
-
| `:not_within` | Range that the value must not fall within |
|
|
2234
|
-
| `:in` | Alias for :within option |
|
|
2235
|
-
| `:not_in` | Alias for :not_within option |
|
|
2236
|
-
| `:min` | Minimum allowed value (inclusive, >=) |
|
|
2237
|
-
| `:max` | Maximum allowed value (inclusive, <=) |
|
|
2238
|
-
| `:is` | Exact value that must match |
|
|
2239
|
-
| `:is_not` | Value that must not match |
|
|
2240
|
-
| `:within_message` | Custom message for range validations |
|
|
2241
|
-
| `:not_within_message` | Custom message for exclusion validations |
|
|
2242
|
-
| `:min_message` | Custom message for minimum validation |
|
|
2243
|
-
| `:max_message` | Custom message for maximum validation |
|
|
2244
|
-
| `:is_message` | Custom message for exact match validation |
|
|
2245
|
-
| `:is_not_message` | Custom message for exclusion validation |
|
|
2246
|
-
|
|
2247
|
-
### Presence
|
|
2248
|
-
|
|
2249
|
-
```ruby
|
|
2250
|
-
class CreateBlogPost < CMDx::Task
|
|
2251
|
-
attribute :content, presence: true
|
|
2252
|
-
|
|
2253
|
-
attribute :content, presence: { message: "cannot be blank" }
|
|
2254
|
-
|
|
2255
|
-
def work
|
|
2256
|
-
# Your logic here...
|
|
2257
|
-
end
|
|
2258
|
-
end
|
|
2259
|
-
```
|
|
2260
|
-
|
|
2261
|
-
| Options | Description |
|
|
2262
|
-
|---------|-------------|
|
|
2263
|
-
| `true` | Ensures value is not nil, empty string, or whitespace |
|
|
2264
|
-
|
|
2265
|
-
## Declarations
|
|
2266
|
-
|
|
2267
|
-
!!! warning "Important"
|
|
2268
|
-
|
|
2269
|
-
Custom validators must raise `CMDx::ValidationError` with a descriptive message.
|
|
2270
|
-
|
|
2271
|
-
### Proc or Lambda
|
|
2272
|
-
|
|
2273
|
-
Use anonymous functions for simple validation logic:
|
|
2274
|
-
|
|
2275
|
-
```ruby
|
|
2276
|
-
class SetupApplication < CMDx::Task
|
|
2277
|
-
# Proc
|
|
2278
|
-
register :validator, :api_key, proc do |value, options = {}|
|
|
2279
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
2280
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
2281
|
-
end
|
|
2282
|
-
end
|
|
2283
|
-
|
|
2284
|
-
# Lambda
|
|
2285
|
-
register :validator, :api_key, ->(value, options = {}) {
|
|
2286
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
2287
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
2288
|
-
end
|
|
2289
|
-
}
|
|
2290
|
-
end
|
|
2291
|
-
```
|
|
2292
|
-
|
|
2293
|
-
### Class or Module
|
|
2294
|
-
|
|
2295
|
-
Register custom validation logic for specialized requirements:
|
|
2296
|
-
|
|
2297
|
-
```ruby
|
|
2298
|
-
class ApiKeyValidator
|
|
2299
|
-
def self.call(value, options = {})
|
|
2300
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
2301
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
2302
|
-
end
|
|
2303
|
-
end
|
|
2304
|
-
end
|
|
2305
|
-
|
|
2306
|
-
class SetupApplication < CMDx::Task
|
|
2307
|
-
register :validator, :api_key, ApiKeyValidator
|
|
2308
|
-
|
|
2309
|
-
attribute :access_key, api_key: true
|
|
2310
|
-
end
|
|
2311
|
-
```
|
|
2312
|
-
|
|
2313
|
-
## Removals
|
|
2314
|
-
|
|
2315
|
-
Remove unwanted validators:
|
|
2316
|
-
|
|
2317
|
-
!!! warning
|
|
2318
|
-
|
|
2319
|
-
Each `deregister` call removes one validator. Use multiple calls for batch removals.
|
|
2320
|
-
|
|
2321
|
-
```ruby
|
|
2322
|
-
class SetupApplication < CMDx::Task
|
|
2323
|
-
deregister :validator, :api_key
|
|
2324
|
-
end
|
|
2325
|
-
```
|
|
2326
|
-
|
|
2327
|
-
## Error Handling
|
|
2328
|
-
|
|
2329
|
-
Validation failures provide detailed, structured error messages:
|
|
2330
|
-
|
|
2331
|
-
```ruby
|
|
2332
|
-
class CreateProject < CMDx::Task
|
|
2333
|
-
attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 }
|
|
2334
|
-
attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 }
|
|
2335
|
-
attribute :priority, inclusion: { in: [:low, :medium, :high] }
|
|
2336
|
-
attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
2337
|
-
|
|
2338
|
-
def work
|
|
2339
|
-
# Your logic here...
|
|
2340
|
-
end
|
|
2341
|
-
end
|
|
2342
|
-
|
|
2343
|
-
result = CreateProject.execute(
|
|
2344
|
-
project_name: "AB", # Too short
|
|
2345
|
-
budget: 500, # Too low
|
|
2346
|
-
priority: :urgent, # Not in allowed list
|
|
2347
|
-
contact_email: "invalid-email" # Invalid format
|
|
2348
|
-
)
|
|
2349
|
-
|
|
2350
|
-
result.state #=> "interrupted"
|
|
2351
|
-
result.status #=> "failed"
|
|
2352
|
-
result.reason #=> "Invalid"
|
|
2353
|
-
result.metadata #=> {
|
|
2354
|
-
# errors: {
|
|
2355
|
-
# full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
|
|
2356
|
-
# messages: {
|
|
2357
|
-
# project_name: ["is too short (minimum is 3 characters)"],
|
|
2358
|
-
# budget: ["must be greater than 1000"],
|
|
2359
|
-
# priority: ["is not included in the list"],
|
|
2360
|
-
# contact_email: ["is invalid"]
|
|
2361
|
-
# }
|
|
2362
|
-
# }
|
|
2363
|
-
# }
|
|
2364
|
-
```
|
|
2365
|
-
|
|
2366
|
-
# Attributes - Defaults
|
|
2367
|
-
|
|
2368
|
-
Provide fallback values for optional attributes. Defaults kick in when values aren't provided or are `nil`.
|
|
2369
|
-
|
|
2370
|
-
## Declarations
|
|
2371
|
-
|
|
2372
|
-
Defaults work seamlessly with coercions, validations, and nested attributes:
|
|
2373
|
-
|
|
2374
|
-
### Static Values
|
|
2375
|
-
|
|
2376
|
-
```ruby
|
|
2377
|
-
class OptimizeDatabase < CMDx::Task
|
|
2378
|
-
attribute :strategy, default: :incremental
|
|
2379
|
-
attribute :level, default: "basic"
|
|
2380
|
-
attribute :notify_admin, default: true
|
|
2381
|
-
attribute :timeout_minutes, default: 30
|
|
2382
|
-
attribute :indexes, default: []
|
|
2383
|
-
attribute :options, default: {}
|
|
2384
|
-
|
|
2385
|
-
def work
|
|
2386
|
-
strategy #=> :incremental
|
|
2387
|
-
level #=> "basic"
|
|
2388
|
-
notify_admin #=> true
|
|
2389
|
-
timeout_minutes #=> 30
|
|
2390
|
-
indexes #=> []
|
|
2391
|
-
options #=> {}
|
|
2392
|
-
end
|
|
2393
|
-
end
|
|
2394
|
-
```
|
|
2395
|
-
|
|
2396
|
-
### Symbol References
|
|
2397
|
-
|
|
2398
|
-
Reference instance methods by symbol for dynamic default values:
|
|
2399
|
-
|
|
2400
|
-
```ruby
|
|
2401
|
-
class ProcessAnalytics < CMDx::Task
|
|
2402
|
-
attribute :granularity, default: :default_granularity
|
|
2403
|
-
|
|
2404
|
-
def work
|
|
2405
|
-
# Your logic here...
|
|
2406
|
-
end
|
|
2407
|
-
|
|
2408
|
-
private
|
|
2409
|
-
|
|
2410
|
-
def default_granularity
|
|
2411
|
-
Current.user.premium? ? "hourly" : "daily"
|
|
2412
|
-
end
|
|
2413
|
-
end
|
|
2414
|
-
```
|
|
2415
|
-
|
|
2416
|
-
### Proc or Lambda
|
|
2417
|
-
|
|
2418
|
-
Use anonymous functions for dynamic default values:
|
|
2419
|
-
|
|
2420
|
-
```ruby
|
|
2421
|
-
class CacheContent < CMDx::Task
|
|
2422
|
-
# Proc
|
|
2423
|
-
attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 }
|
|
2424
|
-
|
|
2425
|
-
# Lambda
|
|
2426
|
-
attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" }
|
|
2427
|
-
end
|
|
2428
|
-
```
|
|
2429
|
-
|
|
2430
|
-
## Coercions and Validations
|
|
2431
|
-
|
|
2432
|
-
Defaults follow the same coercion and validation rules as provided values:
|
|
2433
|
-
|
|
2434
|
-
```ruby
|
|
2435
|
-
class ScheduleBackup < CMDx::Task
|
|
2436
|
-
# Coercions
|
|
2437
|
-
attribute :retention_days, default: "7", type: :integer
|
|
2438
|
-
|
|
2439
|
-
# Validations
|
|
2440
|
-
optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] }
|
|
2441
|
-
end
|
|
2442
|
-
```
|
|
2443
|
-
|
|
2444
|
-
# Attributes - Transformations
|
|
2445
|
-
|
|
2446
|
-
Modify attribute values after coercion but before validation. Perfect for normalization, formatting, and data cleanup.
|
|
2447
|
-
|
|
2448
|
-
## Declarations
|
|
2449
|
-
|
|
2450
|
-
### Symbol References
|
|
2451
|
-
|
|
2452
|
-
Reference instance methods by symbol for dynamic value transformations:
|
|
2453
|
-
|
|
2454
|
-
```ruby
|
|
2455
|
-
class ProcessAnalytics < CMDx::Task
|
|
2456
|
-
attribute :options, transform: :compact_blank
|
|
2457
|
-
end
|
|
2458
|
-
```
|
|
2459
|
-
|
|
2460
|
-
### Proc or Lambda
|
|
2461
|
-
|
|
2462
|
-
Use anonymous functions for dynamic value transformations:
|
|
2463
|
-
|
|
2464
|
-
```ruby
|
|
2465
|
-
class CacheContent < CMDx::Task
|
|
2466
|
-
# Proc
|
|
2467
|
-
attribute :expire_hours, transform: proc { |v| v * 2 }
|
|
2468
|
-
|
|
2469
|
-
# Lambda
|
|
2470
|
-
attribute :compression, transform: ->(v) { v.to_s.upcase.strip[0..2] }
|
|
2471
|
-
end
|
|
2472
|
-
```
|
|
2473
|
-
|
|
2474
|
-
### Class or Module
|
|
2475
|
-
|
|
2476
|
-
Use any object that responds to `call` for reusable transformation logic:
|
|
2477
|
-
|
|
2478
|
-
```ruby
|
|
2479
|
-
class EmailNormalizer
|
|
2480
|
-
def call(value)
|
|
2481
|
-
value.to_s.downcase.strip
|
|
2482
|
-
end
|
|
2483
|
-
end
|
|
2484
|
-
|
|
2485
|
-
class ProcessContacts < CMDx::Task
|
|
2486
|
-
# Class or Module
|
|
2487
|
-
attribute :email, transform: EmailNormalizer
|
|
2488
|
-
|
|
2489
|
-
# Instance
|
|
2490
|
-
attribute :email, transform: EmailNormalizer.new
|
|
2491
|
-
end
|
|
2492
|
-
```
|
|
2493
|
-
|
|
2494
|
-
## Validations
|
|
2495
|
-
|
|
2496
|
-
Validations run on transformed values, ensuring data consistency:
|
|
2497
|
-
|
|
2498
|
-
```ruby
|
|
2499
|
-
class ScheduleBackup < CMDx::Task
|
|
2500
|
-
# Coercions
|
|
2501
|
-
attribute :retention_days, type: :integer, transform: proc { |v| v.clamp(1, 5) }
|
|
2502
|
-
|
|
2503
|
-
# Validations
|
|
2504
|
-
optional :frequency, transform: :downcase, inclusion: { in: %w[hourly daily weekly monthly] }
|
|
2505
|
-
end
|
|
2506
|
-
```
|
|
2507
|
-
|
|
2508
|
-
# Callbacks
|
|
2509
|
-
|
|
2510
|
-
Run custom logic at specific points during task execution. Callbacks have full access to task context and results, making them perfect for logging, notifications, cleanup, and more.
|
|
2511
|
-
|
|
2512
|
-
See [Global Configuration](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#callbacks) for framework-wide callback setup.
|
|
2513
|
-
|
|
2514
|
-
!!! warning "Important"
|
|
2515
|
-
|
|
2516
|
-
Callbacks execute in declaration order (FIFO). Multiple callbacks of the same type run sequentially.
|
|
2517
|
-
|
|
2518
|
-
## Available Callbacks
|
|
2519
|
-
|
|
2520
|
-
Callbacks execute in a predictable lifecycle order:
|
|
2521
|
-
|
|
2522
|
-
```ruby
|
|
2523
|
-
1. before_validation # Pre-validation setup
|
|
2524
|
-
2. before_execution # Prepare for execution
|
|
2525
|
-
|
|
2526
|
-
# --- Task#work executes ---
|
|
2527
|
-
|
|
2528
|
-
3. on_[complete|interrupted] # State-based (execution lifecycle)
|
|
2529
|
-
4. on_executed # Always runs after work completes
|
|
2530
|
-
5. on_[success|skipped|failed] # Status-based (business outcome)
|
|
2531
|
-
6. on_[good|bad] # Outcome-based (success/skip vs fail)
|
|
2532
|
-
```
|
|
2533
|
-
|
|
2534
|
-
## Declarations
|
|
2535
|
-
|
|
2536
|
-
### Symbol References
|
|
2537
|
-
|
|
2538
|
-
Reference instance methods by symbol for simple callback logic:
|
|
2539
|
-
|
|
2540
|
-
```ruby
|
|
2541
|
-
class ProcessBooking < CMDx::Task
|
|
2542
|
-
before_execution :find_reservation
|
|
2543
|
-
|
|
2544
|
-
# Batch declarations (works for any type)
|
|
2545
|
-
on_complete :notify_guest, :update_availability
|
|
2546
|
-
|
|
2547
|
-
def work
|
|
2548
|
-
# Your logic here...
|
|
2549
|
-
end
|
|
2550
|
-
|
|
2551
|
-
private
|
|
2552
|
-
|
|
2553
|
-
def find_reservation
|
|
2554
|
-
@reservation ||= Reservation.find(context.reservation_id)
|
|
2555
|
-
end
|
|
2556
|
-
|
|
2557
|
-
def notify_guest
|
|
2558
|
-
GuestNotifier.call(context.guest, result)
|
|
2559
|
-
end
|
|
2560
|
-
|
|
2561
|
-
def update_availability
|
|
2562
|
-
AvailabilityService.update(context.room_ids, result)
|
|
2563
|
-
end
|
|
2564
|
-
end
|
|
2565
|
-
```
|
|
2566
|
-
|
|
2567
|
-
### Proc or Lambda
|
|
2568
|
-
|
|
2569
|
-
Use anonymous functions for inline callback logic:
|
|
2570
|
-
|
|
2571
|
-
```ruby
|
|
2572
|
-
class ProcessBooking < CMDx::Task
|
|
2573
|
-
# Proc
|
|
2574
|
-
on_interrupted proc { ReservationSystem.pause! }
|
|
2575
|
-
|
|
2576
|
-
# Lambda
|
|
2577
|
-
on_complete -> { ReservationSystem.resume! }
|
|
2578
|
-
end
|
|
2579
|
-
```
|
|
2580
|
-
|
|
2581
|
-
### Class or Module
|
|
2582
|
-
|
|
2583
|
-
Implement reusable callback logic in dedicated modules and classes:
|
|
2584
|
-
|
|
2585
|
-
```ruby
|
|
2586
|
-
class BookingConfirmationCallback
|
|
2587
|
-
def call(task)
|
|
2588
|
-
if task.result.success?
|
|
2589
|
-
MessagingApi.send_confirmation(task.context.guest)
|
|
2590
|
-
else
|
|
2591
|
-
MessagingApi.send_issue_alert(task.context.manager)
|
|
2592
|
-
end
|
|
2593
|
-
end
|
|
2594
|
-
end
|
|
2595
|
-
|
|
2596
|
-
class ProcessBooking < CMDx::Task
|
|
2597
|
-
# Class or Module
|
|
2598
|
-
on_success BookingConfirmationCallback
|
|
2599
|
-
|
|
2600
|
-
# Instance
|
|
2601
|
-
on_interrupted BookingConfirmationCallback.new
|
|
2602
|
-
end
|
|
2603
|
-
```
|
|
2604
|
-
|
|
2605
|
-
### Conditional Execution
|
|
2606
|
-
|
|
2607
|
-
Control callback execution with conditional logic:
|
|
2608
|
-
|
|
2609
|
-
```ruby
|
|
2610
|
-
class MessagingPermissionCheck
|
|
2611
|
-
def call(task)
|
|
2612
|
-
task.context.guest.can?(:receive_messages)
|
|
2613
|
-
end
|
|
2614
|
-
end
|
|
2615
|
-
|
|
2616
|
-
class ProcessBooking < CMDx::Task
|
|
2617
|
-
# If and/or Unless
|
|
2618
|
-
before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked?
|
|
2619
|
-
|
|
2620
|
-
# Proc
|
|
2621
|
-
on_failure :increment_failure, if: -> { Rails.env.production? && self.class.name.include?("Legacy") }
|
|
2622
|
-
|
|
2623
|
-
# Lambda
|
|
2624
|
-
on_success :ping_housekeeping, if: proc { context.rooms_need_cleaning? }
|
|
2625
|
-
|
|
2626
|
-
# Class or Module
|
|
2627
|
-
on_complete :send_confirmation, unless: MessagingPermissionCheck
|
|
2628
|
-
|
|
2629
|
-
# Instance
|
|
2630
|
-
on_complete :send_confirmation, if: MessagingPermissionCheck.new
|
|
2631
|
-
|
|
2632
|
-
def work
|
|
2633
|
-
# Your logic here...
|
|
2634
|
-
end
|
|
2635
|
-
|
|
2636
|
-
private
|
|
2637
|
-
|
|
2638
|
-
def messaging_enabled?
|
|
2639
|
-
context.guest.messaging_preference == true
|
|
2640
|
-
end
|
|
2641
|
-
|
|
2642
|
-
def messaging_blocked?
|
|
2643
|
-
context.guest.communication_status == :blocked
|
|
2644
|
-
end
|
|
2645
|
-
end
|
|
2646
|
-
```
|
|
2647
|
-
|
|
2648
|
-
## Callback Removal
|
|
2649
|
-
|
|
2650
|
-
Remove unwanted callbacks dynamically:
|
|
2651
|
-
|
|
2652
|
-
!!! warning "Important"
|
|
2653
|
-
|
|
2654
|
-
Each `deregister` call removes one callback. Use multiple calls for batch removals.
|
|
2655
|
-
|
|
2656
|
-
```ruby
|
|
2657
|
-
class ProcessBooking < CMDx::Task
|
|
2658
|
-
# Symbol
|
|
2659
|
-
deregister :callback, :before_execution, :notify_guest
|
|
2660
|
-
|
|
2661
|
-
# Class or Module (no instances)
|
|
2662
|
-
deregister :callback, :on_complete, BookingConfirmationCallback
|
|
2663
|
-
end
|
|
2664
|
-
```
|
|
2665
|
-
|
|
2666
|
-
# Middlewares
|
|
2667
|
-
|
|
2668
|
-
Wrap task execution with middleware for cross-cutting concerns like authentication, caching, timeouts, and monitoring. Think Rack middleware, but for your business logic.
|
|
2669
|
-
|
|
2670
|
-
See [Global Configuration](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#middlewares) for framework-wide setup.
|
|
2671
|
-
|
|
2672
|
-
## Execution Order
|
|
2673
|
-
|
|
2674
|
-
Middleware wraps task execution in layers, like an onion:
|
|
2675
|
-
|
|
2676
|
-
!!! note
|
|
2677
|
-
|
|
2678
|
-
First registered = outermost wrapper. They execute in registration order.
|
|
2679
|
-
|
|
2680
|
-
```ruby
|
|
2681
|
-
class ProcessCampaign < CMDx::Task
|
|
2682
|
-
register :middleware, AuditMiddleware # 1st: outermost wrapper
|
|
2683
|
-
register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
|
|
2684
|
-
register :middleware, CacheMiddleware # 3rd: innermost wrapper
|
|
2685
|
-
|
|
2686
|
-
def work
|
|
2687
|
-
# Your logic here...
|
|
2688
|
-
end
|
|
2689
|
-
end
|
|
2690
|
-
|
|
2691
|
-
# Execution flow:
|
|
2692
|
-
# 1. AuditMiddleware (before)
|
|
2693
|
-
# 2. AuthorizationMiddleware (before)
|
|
2694
|
-
# 3. CacheMiddleware (before)
|
|
2695
|
-
# 4. [task execution]
|
|
2696
|
-
# 5. CacheMiddleware (after)
|
|
2697
|
-
# 6. AuthorizationMiddleware (after)
|
|
2698
|
-
# 7. AuditMiddleware (after)
|
|
2699
|
-
```
|
|
2700
|
-
|
|
2701
|
-
## Declarations
|
|
2702
|
-
|
|
2703
|
-
### Proc or Lambda
|
|
2704
|
-
|
|
2705
|
-
Use anonymous functions for simple middleware logic:
|
|
2706
|
-
|
|
2707
|
-
```ruby
|
|
2708
|
-
class ProcessCampaign < CMDx::Task
|
|
2709
|
-
# Proc
|
|
2710
|
-
register :middleware, proc do |task, options, &block|
|
|
2711
|
-
result = block.call
|
|
2712
|
-
Analytics.track(result.status)
|
|
2713
|
-
result
|
|
2714
|
-
end
|
|
2715
|
-
|
|
2716
|
-
# Lambda
|
|
2717
|
-
register :middleware, ->(task, options, &block) {
|
|
2718
|
-
result = block.call
|
|
2719
|
-
Analytics.track(result.status)
|
|
2720
|
-
result
|
|
2721
|
-
}
|
|
2722
|
-
end
|
|
2723
|
-
```
|
|
2724
|
-
|
|
2725
|
-
### Class or Module
|
|
2726
|
-
|
|
2727
|
-
For complex middleware logic, use classes or modules:
|
|
2728
|
-
|
|
2729
|
-
```ruby
|
|
2730
|
-
class TelemetryMiddleware
|
|
2731
|
-
def call(task, options)
|
|
2732
|
-
result = yield
|
|
2733
|
-
Telemetry.record(result.status)
|
|
2734
|
-
ensure
|
|
2735
|
-
result # Always return result
|
|
2736
|
-
end
|
|
2737
|
-
end
|
|
2738
|
-
|
|
2739
|
-
class ProcessCampaign < CMDx::Task
|
|
2740
|
-
# Class or Module
|
|
2741
|
-
register :middleware, TelemetryMiddleware
|
|
2742
|
-
|
|
2743
|
-
# Instance
|
|
2744
|
-
register :middleware, TelemetryMiddleware.new
|
|
2745
|
-
|
|
2746
|
-
# With options
|
|
2747
|
-
register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
|
|
2748
|
-
register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
|
|
2749
|
-
end
|
|
2750
|
-
```
|
|
2751
|
-
|
|
2752
|
-
## Removals
|
|
2753
|
-
|
|
2754
|
-
Remove class or module-based middleware globally or per-task:
|
|
2755
|
-
|
|
2756
|
-
!!! warning
|
|
2757
|
-
|
|
2758
|
-
Each `deregister` call removes one middleware. Use multiple calls for batch removals.
|
|
2759
|
-
|
|
2760
|
-
```ruby
|
|
2761
|
-
class ProcessCampaign < CMDx::Task
|
|
2762
|
-
# Class or Module (no instances)
|
|
2763
|
-
deregister :middleware, TelemetryMiddleware
|
|
2764
|
-
end
|
|
2765
|
-
```
|
|
2766
|
-
|
|
2767
|
-
## Built-in
|
|
2768
|
-
|
|
2769
|
-
### Timeout
|
|
2770
|
-
|
|
2771
|
-
Prevent tasks from running too long:
|
|
2772
|
-
|
|
2773
|
-
```ruby
|
|
2774
|
-
class ProcessReport < CMDx::Task
|
|
2775
|
-
# Default timeout: 3 seconds
|
|
2776
|
-
register :middleware, CMDx::Middlewares::Timeout
|
|
2777
|
-
|
|
2778
|
-
# Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
|
|
2779
|
-
register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
|
|
2780
|
-
|
|
2781
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
2782
|
-
register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
|
|
2783
|
-
|
|
2784
|
-
def work
|
|
2785
|
-
# Your logic here...
|
|
2786
|
-
end
|
|
2787
|
-
|
|
2788
|
-
private
|
|
2789
|
-
|
|
2790
|
-
def max_processing_time
|
|
2791
|
-
Rails.env.production? ? 2 : 10
|
|
2792
|
-
end
|
|
2793
|
-
end
|
|
2794
|
-
|
|
2795
|
-
# Slow task
|
|
2796
|
-
result = ProcessReport.execute
|
|
2797
|
-
|
|
2798
|
-
result.state #=> "interrupted"
|
|
2799
|
-
result.status #=> "failure"
|
|
2800
|
-
result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
|
|
2801
|
-
result.cause #=> <CMDx::TimeoutError>
|
|
2802
|
-
result.metadata #=> { limit: 3 }
|
|
2803
|
-
```
|
|
2804
|
-
|
|
2805
|
-
### Correlate
|
|
2806
|
-
|
|
2807
|
-
Add correlation IDs for distributed tracing and request tracking:
|
|
2808
|
-
|
|
2809
|
-
```ruby
|
|
2810
|
-
class ProcessExport < CMDx::Task
|
|
2811
|
-
# Default correlation ID generation
|
|
2812
|
-
register :middleware, CMDx::Middlewares::Correlate
|
|
2813
|
-
|
|
2814
|
-
# Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
|
|
2815
|
-
register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
|
|
2816
|
-
|
|
2817
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
2818
|
-
register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
|
|
2819
|
-
|
|
2820
|
-
def work
|
|
2821
|
-
# Your logic here...
|
|
2822
|
-
end
|
|
2823
|
-
|
|
2824
|
-
private
|
|
2825
|
-
|
|
2826
|
-
def correlation_enabled?
|
|
2827
|
-
ENV["CORRELATION_ENABLED"] == "true"
|
|
2828
|
-
end
|
|
2829
|
-
end
|
|
2830
|
-
|
|
2831
|
-
result = ProcessExport.execute
|
|
2832
|
-
result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
|
|
2833
|
-
```
|
|
2834
|
-
|
|
2835
|
-
### Runtime
|
|
2836
|
-
|
|
2837
|
-
Track task execution time in milliseconds using a monotonic clock:
|
|
2838
|
-
|
|
2839
|
-
```ruby
|
|
2840
|
-
class PerformanceMonitoringCheck
|
|
2841
|
-
def call(task)
|
|
2842
|
-
task.context.tenant.monitoring_enabled?
|
|
2843
|
-
end
|
|
2844
|
-
end
|
|
2845
|
-
|
|
2846
|
-
class ProcessExport < CMDx::Task
|
|
2847
|
-
# Default timeout is 3 seconds
|
|
2848
|
-
register :middleware, CMDx::Middlewares::Runtime
|
|
2849
|
-
|
|
2850
|
-
# If or Unless (takes Symbol, Proc, Lambda, Class, Module)
|
|
2851
|
-
register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
|
|
2852
|
-
end
|
|
2853
|
-
|
|
2854
|
-
result = ProcessExport.execute
|
|
2855
|
-
result.metadata #=> { runtime: 1247 } (ms)
|
|
2856
|
-
```
|
|
2857
|
-
|
|
2858
|
-
# Logging
|
|
2859
|
-
|
|
2860
|
-
CMDx automatically logs every task execution with structured data, making debugging and monitoring effortless. Choose from multiple formatters to match your logging infrastructure.
|
|
2861
|
-
|
|
2862
|
-
## Formatters
|
|
2863
|
-
|
|
2864
|
-
Choose the format that works best for your logging system:
|
|
2865
|
-
|
|
2866
|
-
| Formatter | Use Case | Output Style |
|
|
2867
|
-
|-----------|----------|--------------|
|
|
2868
|
-
| `Line` | Traditional logging | Single-line format |
|
|
2869
|
-
| `Json` | Structured systems | Compact JSON |
|
|
2870
|
-
| `KeyValue` | Log parsing | `key=value` pairs |
|
|
2871
|
-
| `Logstash` | ELK stack | JSON with @version/@timestamp |
|
|
2872
|
-
| `Raw` | Minimal output | Message content only |
|
|
2873
|
-
|
|
2874
|
-
Sample output:
|
|
2875
|
-
|
|
2876
|
-
```log
|
|
2877
|
-
<!-- Success (INFO level) -->
|
|
2878
|
-
I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
|
|
2879
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
|
2880
|
-
|
|
2881
|
-
<!-- Skipped (WARN level) -->
|
|
2882
|
-
W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
|
|
2883
|
-
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
|
2884
|
-
|
|
2885
|
-
<!-- Failed (ERROR level) -->
|
|
2886
|
-
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
|
|
2887
|
-
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
|
2888
|
-
|
|
2889
|
-
<!-- Failed Chain -->
|
|
2890
|
-
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
|
|
2891
|
-
index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
|
2892
|
-
```
|
|
2893
|
-
|
|
2894
|
-
!!! tip
|
|
2895
|
-
|
|
2896
|
-
Use logging as a low-level event stream to track all tasks in a request. Combine with correlation for powerful distributed tracing.
|
|
2897
|
-
|
|
2898
|
-
## Structure
|
|
2899
|
-
|
|
2900
|
-
Every log entry includes rich metadata. Available fields depend on execution context and outcome.
|
|
2901
|
-
|
|
2902
|
-
### Core Fields
|
|
2903
|
-
|
|
2904
|
-
| Field | Description | Example |
|
|
2905
|
-
|-------|-------------|---------|
|
|
2906
|
-
| `severity` | Log level | `INFO`, `WARN`, `ERROR` |
|
|
2907
|
-
| `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
|
|
2908
|
-
| `pid` | Process ID | `3784` |
|
|
2909
|
-
|
|
2910
|
-
### Task Information
|
|
2911
|
-
|
|
2912
|
-
| Field | Description | Example |
|
|
2913
|
-
|-------|-------------|---------|
|
|
2914
|
-
| `index` | Execution sequence position | `0`, `1`, `2` |
|
|
2915
|
-
| `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
|
|
2916
|
-
| `type` | Execution unit type | `Task`, `Workflow` |
|
|
2917
|
-
| `class` | Task class name | `GenerateInvoiceTask` |
|
|
2918
|
-
| `id` | Unique task instance ID | `018c2b95-b764-7615...` |
|
|
2919
|
-
| `tags` | Custom categorization | `["billing", "financial"]` |
|
|
2920
|
-
|
|
2921
|
-
### Execution Data
|
|
2922
|
-
|
|
2923
|
-
| Field | Description | Example |
|
|
2924
|
-
|-------|-------------|---------|
|
|
2925
|
-
| `state` | Lifecycle state | `complete`, `interrupted` |
|
|
2926
|
-
| `status` | Business outcome | `success`, `skipped`, `failed` |
|
|
2927
|
-
| `outcome` | Final classification | `success`, `interrupted` |
|
|
2928
|
-
| `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
|
|
2929
|
-
|
|
2930
|
-
### Failure Chain
|
|
2931
|
-
|
|
2932
|
-
| Field | Description |
|
|
2933
|
-
|-------|-------------|
|
|
2934
|
-
| `reason` | Reason given for the stoppage |
|
|
2935
|
-
| `caused` | Cause exception details |
|
|
2936
|
-
| `caused_failure` | Original failing task details |
|
|
2937
|
-
| `threw_failure` | Task that propagated the failure |
|
|
2938
|
-
|
|
2939
|
-
## Usage
|
|
2940
|
-
|
|
2941
|
-
Access the framework logger directly within tasks:
|
|
2942
|
-
|
|
2943
|
-
```ruby
|
|
2944
|
-
class ProcessSubscription < CMDx::Task
|
|
2945
|
-
def work
|
|
2946
|
-
logger.debug { "Activated feature flags: #{Features.active_flags}" }
|
|
2947
|
-
# Your logic here...
|
|
2948
|
-
logger.info("Subscription processed")
|
|
2949
|
-
end
|
|
2950
|
-
end
|
|
2951
|
-
```
|
|
2952
|
-
|
|
2953
|
-
# Internationalization (i18n)
|
|
2954
|
-
|
|
2955
|
-
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.
|
|
2956
|
-
|
|
2957
|
-
## Usage
|
|
2958
|
-
|
|
2959
|
-
All error messages are automatically localized based on your current locale:
|
|
2960
|
-
|
|
2961
|
-
```ruby
|
|
2962
|
-
class ProcessQuote < CMDx::Task
|
|
2963
|
-
attribute :price, type: :float
|
|
2964
|
-
|
|
2965
|
-
def work
|
|
2966
|
-
# Your logic here...
|
|
2967
|
-
end
|
|
2968
|
-
end
|
|
2969
|
-
|
|
2970
|
-
I18n.with_locale(:fr) do
|
|
2971
|
-
result = ProcessQuote.execute(price: "invalid")
|
|
2972
|
-
result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
|
|
2973
|
-
end
|
|
2974
|
-
```
|
|
2975
|
-
|
|
2976
|
-
## Configuration
|
|
2977
|
-
|
|
2978
|
-
CMDx uses the `I18n` gem for localization. In Rails, locales load automatically.
|
|
2979
|
-
|
|
2980
|
-
### Copy Locale Files
|
|
2981
|
-
|
|
2982
|
-
Copy locale files to your Rails application's `config/locales` directory:
|
|
2983
|
-
|
|
2984
|
-
```bash
|
|
2985
|
-
rails generate cmdx:locale [LOCALE]
|
|
2986
|
-
|
|
2987
|
-
# Eg: generate french locale
|
|
2988
|
-
rails generate cmdx:locale fr
|
|
2989
|
-
```
|
|
2990
|
-
|
|
2991
|
-
### Available Locales
|
|
2992
|
-
|
|
2993
|
-
- af - Afrikaans
|
|
2994
|
-
- ar - Arabic
|
|
2995
|
-
- az - Azerbaijani
|
|
2996
|
-
- be - Belarusian
|
|
2997
|
-
- bg - Bulgarian
|
|
2998
|
-
- bn - Bengali
|
|
2999
|
-
- bs - Bosnian
|
|
3000
|
-
- ca - Catalan
|
|
3001
|
-
- cnr - Montenegrin
|
|
3002
|
-
- cs - Czech
|
|
3003
|
-
- cy - Welsh
|
|
3004
|
-
- da - Danish
|
|
3005
|
-
- de - German
|
|
3006
|
-
- dz - Dzongkha
|
|
3007
|
-
- el - Greek
|
|
3008
|
-
- en - English
|
|
3009
|
-
- eo - Esperanto
|
|
3010
|
-
- es - Spanish
|
|
3011
|
-
- et - Estonian
|
|
3012
|
-
- eu - Basque
|
|
3013
|
-
- fa - Persian
|
|
3014
|
-
- fi - Finnish
|
|
3015
|
-
- fr - French
|
|
3016
|
-
- fy - Western Frisian
|
|
3017
|
-
- gd - Scottish Gaelic
|
|
3018
|
-
- gl - Galician
|
|
3019
|
-
- he - Hebrew
|
|
3020
|
-
- hi - Hindi
|
|
3021
|
-
- hr - Croatian
|
|
3022
|
-
- hu - Hungarian
|
|
3023
|
-
- hy - Armenian
|
|
3024
|
-
- id - Indonesian
|
|
3025
|
-
- is - Icelandic
|
|
3026
|
-
- it - Italian
|
|
3027
|
-
- ja - Japanese
|
|
3028
|
-
- ka - Georgian
|
|
3029
|
-
- kk - Kazakh
|
|
3030
|
-
- km - Khmer
|
|
3031
|
-
- kn - Kannada
|
|
3032
|
-
- ko - Korean
|
|
3033
|
-
- lb - Luxembourgish
|
|
3034
|
-
- lo - Lao
|
|
3035
|
-
- lt - Lithuanian
|
|
3036
|
-
- lv - Latvian
|
|
3037
|
-
- mg - Malagasy
|
|
3038
|
-
- mk - Macedonian
|
|
3039
|
-
- ml - Malayalam
|
|
3040
|
-
- mn - Mongolian
|
|
3041
|
-
- mr-IN - Marathi (India)
|
|
3042
|
-
- ms - Malay
|
|
3043
|
-
- nb - Norwegian BokmΓ₯l
|
|
3044
|
-
- ne - Nepali
|
|
3045
|
-
- nl - Dutch
|
|
3046
|
-
- nn - Norwegian Nynorsk
|
|
3047
|
-
- oc - Occitan
|
|
3048
|
-
- or - Odia
|
|
3049
|
-
- pa - Punjabi
|
|
3050
|
-
- pl - Polish
|
|
3051
|
-
- pt - Portuguese
|
|
3052
|
-
- rm - Romansh
|
|
3053
|
-
- ro - Romanian
|
|
3054
|
-
- ru - Russian
|
|
3055
|
-
- sc - Sardinian
|
|
3056
|
-
- sk - Slovak
|
|
3057
|
-
- sl - Slovenian
|
|
3058
|
-
- sq - Albanian
|
|
3059
|
-
- sr - Serbian
|
|
3060
|
-
- st - Southern Sotho
|
|
3061
|
-
- sv - Swedish
|
|
3062
|
-
- sw - Swahili
|
|
3063
|
-
- ta - Tamil
|
|
3064
|
-
- te - Telugu
|
|
3065
|
-
- th - Thai
|
|
3066
|
-
- tl - Tagalog
|
|
3067
|
-
- tr - Turkish
|
|
3068
|
-
- tt - Tatar
|
|
3069
|
-
- ug - Uyghur
|
|
3070
|
-
- uk - Ukrainian
|
|
3071
|
-
- ur - Urdu
|
|
3072
|
-
- uz - Uzbek
|
|
3073
|
-
- vi - Vietnamese
|
|
3074
|
-
- wo - Wolof
|
|
3075
|
-
- zh-CN - Chinese (Simplified)
|
|
3076
|
-
- zh-HK - Chinese (Hong Kong)
|
|
3077
|
-
- zh-TW - Chinese (Traditional)
|
|
3078
|
-
- zh-YUE - Chinese (Yue)
|
|
3079
|
-
|
|
3080
|
-
# Retries
|
|
3081
|
-
|
|
3082
|
-
CMDx provides automatic retry functionality for tasks that encounter transient failures. This is essential for handling temporary issues like network timeouts, rate limits, or database locks without manual intervention.
|
|
3083
|
-
|
|
3084
|
-
## Basic Usage
|
|
3085
|
-
|
|
3086
|
-
Configure retries upto n attempts without any delay.
|
|
3087
|
-
|
|
3088
|
-
```ruby
|
|
3089
|
-
class FetchExternalData < CMDx::Task
|
|
3090
|
-
settings retries: 3
|
|
3091
|
-
|
|
3092
|
-
def work
|
|
3093
|
-
response = HTTParty.get("https://api.example.com/data")
|
|
3094
|
-
context.data = response.parsed_response
|
|
3095
|
-
end
|
|
3096
|
-
end
|
|
3097
|
-
```
|
|
3098
|
-
|
|
3099
|
-
When an exception occurs during execution, CMDx automatically retries up to the configured limit.
|
|
3100
|
-
|
|
3101
|
-
## Selective Retries
|
|
3102
|
-
|
|
3103
|
-
By default, CMDx retries on `StandardError` and its subclasses. Narrow this to specific exception types:
|
|
3104
|
-
|
|
3105
|
-
```ruby
|
|
3106
|
-
class ProcessPayment < CMDx::Task
|
|
3107
|
-
settings retries: 5, retry_on: [Stripe::RateLimitError, Net::ReadTimeout]
|
|
3108
|
-
|
|
3109
|
-
def work
|
|
3110
|
-
# Your logic here...
|
|
3111
|
-
end
|
|
3112
|
-
end
|
|
3113
|
-
```
|
|
3114
|
-
|
|
3115
|
-
!!! warning "Important"
|
|
3116
|
-
|
|
3117
|
-
Only exceptions matching the `retry_on` configuration will trigger retries. Uncaught exceptions immediately fail the task.
|
|
3118
|
-
|
|
3119
|
-
## Retry Jitter
|
|
3120
|
-
|
|
3121
|
-
Add delays between retry attempts to avoid overwhelming external services or to implement exponential backoff strategies.
|
|
3122
|
-
|
|
3123
|
-
### Fixed Value
|
|
3124
|
-
|
|
3125
|
-
Use a numeric value to calculate linear delay (`jitter * current_retry`):
|
|
3126
|
-
|
|
3127
|
-
```ruby
|
|
3128
|
-
class ImportRecords < CMDx::Task
|
|
3129
|
-
settings retries: 3, retry_jitter: 0.5
|
|
3130
|
-
|
|
3131
|
-
def work
|
|
3132
|
-
# Delays: 0s, 0.5s (retry 1), 1.0s (retry 2), 1.5s (retry 3)
|
|
3133
|
-
context.records = ExternalAPI.fetch_records
|
|
3134
|
-
end
|
|
3135
|
-
end
|
|
3136
|
-
```
|
|
3137
|
-
|
|
3138
|
-
### Symbol References
|
|
3139
|
-
|
|
3140
|
-
Define an instance method for custom delay logic:
|
|
3141
|
-
|
|
3142
|
-
```ruby
|
|
3143
|
-
class SyncInventory < CMDx::Task
|
|
3144
|
-
settings retries: 5, retry_jitter: :exponential_backoff
|
|
3145
|
-
|
|
3146
|
-
def work
|
|
3147
|
-
context.inventory = InventoryAPI.sync
|
|
3148
|
-
end
|
|
3149
|
-
|
|
3150
|
-
private
|
|
3151
|
-
|
|
3152
|
-
def exponential_backoff(current_retry)
|
|
3153
|
-
2 ** current_retry # 2s, 4s, 8s, 16s, 32s
|
|
3154
|
-
end
|
|
3155
|
-
end
|
|
3156
|
-
```
|
|
3157
|
-
|
|
3158
|
-
### Proc or Lambda
|
|
3159
|
-
|
|
3160
|
-
Pass a proc for inline delay calculations:
|
|
3161
|
-
|
|
3162
|
-
```ruby
|
|
3163
|
-
class PollJobStatus < CMDx::Task
|
|
3164
|
-
# Proc
|
|
3165
|
-
settings retries: 10, retry_jitter: proc { |retry_count| [retry_count * 0.5, 5.0].min }
|
|
3166
|
-
|
|
3167
|
-
# Lambda
|
|
3168
|
-
settings retries: 10, retry_jitter: ->(retry_count) { [retry_count * 0.5, 5.0].min }
|
|
3169
|
-
|
|
3170
|
-
def work
|
|
3171
|
-
# Delays: 0.5s, 1.0s, 1.5s, 2.0s, 2.5s, 3.0s, 3.5s, 4.0s, 4.5s, 5.0s (capped)
|
|
3172
|
-
context.status = JobAPI.check_status(context.job_id)
|
|
3173
|
-
end
|
|
3174
|
-
end
|
|
3175
|
-
```
|
|
3176
|
-
|
|
3177
|
-
### Class or Module
|
|
3178
|
-
|
|
3179
|
-
Implement reusable delay logic in dedicated modules and classes:
|
|
3180
|
-
|
|
3181
|
-
```ruby
|
|
3182
|
-
class ExponentialBackoff
|
|
3183
|
-
def call(task, retry_count)
|
|
3184
|
-
base_delay = task.context.base_delay || 1.0
|
|
3185
|
-
[base_delay * (2 ** retry_count), 60.0].min
|
|
3186
|
-
end
|
|
3187
|
-
end
|
|
3188
|
-
|
|
3189
|
-
class FetchUserProfile < CMDx::Task
|
|
3190
|
-
# Class or Module
|
|
3191
|
-
settings retries: 4, retry_jitter: ExponentialBackoff
|
|
3192
|
-
|
|
3193
|
-
# Instance
|
|
3194
|
-
settings retries: 4, retry_jitter: ExponentialBackoff.new
|
|
3195
|
-
|
|
3196
|
-
def work
|
|
3197
|
-
# Your logic here...
|
|
3198
|
-
end
|
|
3199
|
-
end
|
|
3200
|
-
```
|
|
3201
|
-
|
|
3202
|
-
# Task Deprecation
|
|
3203
|
-
|
|
3204
|
-
Manage legacy tasks gracefully with built-in deprecation support. Choose how to handle deprecated tasksβlog warnings for awareness, issue Ruby warnings for development, or prevent execution entirely.
|
|
3205
|
-
|
|
3206
|
-
## Modes
|
|
3207
|
-
|
|
3208
|
-
### Raise
|
|
3209
|
-
|
|
3210
|
-
Prevent task execution completely. Perfect for tasks that must no longer run.
|
|
3211
|
-
|
|
3212
|
-
!!! warning
|
|
3213
|
-
|
|
3214
|
-
Use `:raise` mode carefullyβit will break existing workflows immediately.
|
|
3215
|
-
|
|
3216
|
-
```ruby
|
|
3217
|
-
class ProcessObsoleteAPI < CMDx::Task
|
|
3218
|
-
settings(deprecated: :raise)
|
|
3219
|
-
|
|
3220
|
-
def work
|
|
3221
|
-
# Will never execute...
|
|
3222
|
-
end
|
|
3223
|
-
end
|
|
3224
|
-
|
|
3225
|
-
result = ProcessObsoleteAPI.execute
|
|
3226
|
-
#=> raises CMDx::DeprecationError: "ProcessObsoleteAPI usage prohibited"
|
|
3227
|
-
```
|
|
3228
|
-
|
|
3229
|
-
### Log
|
|
3230
|
-
|
|
3231
|
-
Allow execution while tracking deprecation in logs. Ideal for gradual migrations.
|
|
3232
|
-
|
|
3233
|
-
```ruby
|
|
3234
|
-
class ProcessLegacyFormat < CMDx::Task
|
|
3235
|
-
settings(deprecated: :log)
|
|
3236
|
-
|
|
3237
|
-
# Same
|
|
3238
|
-
settings(deprecated: true)
|
|
3239
|
-
|
|
3240
|
-
def work
|
|
3241
|
-
# Executes but logs deprecation warning...
|
|
3242
|
-
end
|
|
3243
|
-
end
|
|
3244
|
-
|
|
3245
|
-
result = ProcessLegacyFormat.execute
|
|
3246
|
-
result.successful? #=> true
|
|
3247
|
-
|
|
3248
|
-
# Deprecation warning appears in logs:
|
|
3249
|
-
# WARN -- : DEPRECATED: ProcessLegacyFormat - migrate to replacement or discontinue use
|
|
3250
|
-
```
|
|
3251
|
-
|
|
3252
|
-
### Warn
|
|
3253
|
-
|
|
3254
|
-
Issue Ruby warnings visible during development and testing. Keeps production logs clean while alerting developers.
|
|
3255
|
-
|
|
3256
|
-
```ruby
|
|
3257
|
-
class ProcessOldData < CMDx::Task
|
|
3258
|
-
settings(deprecated: :warn)
|
|
3259
|
-
|
|
3260
|
-
def work
|
|
3261
|
-
# Executes but emits Ruby warning...
|
|
3262
|
-
end
|
|
3263
|
-
end
|
|
3264
|
-
|
|
3265
|
-
result = ProcessOldData.execute
|
|
3266
|
-
result.successful? #=> true
|
|
3267
|
-
|
|
3268
|
-
# Ruby warning appears in stderr:
|
|
3269
|
-
# [ProcessOldData] DEPRECATED: migrate to a replacement or discontinue use
|
|
3270
|
-
```
|
|
3271
|
-
|
|
3272
|
-
## Declarations
|
|
3273
|
-
|
|
3274
|
-
### Symbol or String
|
|
3275
|
-
|
|
3276
|
-
```ruby
|
|
3277
|
-
class OutdatedConnector < CMDx::Task
|
|
3278
|
-
# Symbol
|
|
3279
|
-
settings(deprecated: :raise)
|
|
3280
|
-
|
|
3281
|
-
# String
|
|
3282
|
-
settings(deprecated: "warn")
|
|
3283
|
-
end
|
|
3284
|
-
```
|
|
3285
|
-
|
|
3286
|
-
### Boolean or Nil
|
|
3287
|
-
|
|
3288
|
-
```ruby
|
|
3289
|
-
class OutdatedConnector < CMDx::Task
|
|
3290
|
-
# Deprecates with default :log mode
|
|
3291
|
-
settings(deprecated: true)
|
|
3292
|
-
|
|
3293
|
-
# Skips deprecation
|
|
3294
|
-
settings(deprecated: false)
|
|
3295
|
-
settings(deprecated: nil)
|
|
3296
|
-
end
|
|
3297
|
-
```
|
|
3298
|
-
|
|
3299
|
-
### Method
|
|
3300
|
-
|
|
3301
|
-
```ruby
|
|
3302
|
-
class OutdatedConnector < CMDx::Task
|
|
3303
|
-
# Symbol
|
|
3304
|
-
settings(deprecated: :deprecated?)
|
|
3305
|
-
|
|
3306
|
-
def work
|
|
3307
|
-
# Your logic here...
|
|
3308
|
-
end
|
|
3309
|
-
|
|
3310
|
-
private
|
|
3311
|
-
|
|
3312
|
-
def deprecated?
|
|
3313
|
-
Time.now.year > 2024 ? :raise : false
|
|
3314
|
-
end
|
|
3315
|
-
end
|
|
3316
|
-
```
|
|
3317
|
-
|
|
3318
|
-
### Proc or Lambda
|
|
3319
|
-
|
|
3320
|
-
```ruby
|
|
3321
|
-
class OutdatedConnector < CMDx::Task
|
|
3322
|
-
# Proc
|
|
3323
|
-
settings(deprecated: proc { Rails.env.development? ? :raise : :log })
|
|
3324
|
-
|
|
3325
|
-
# Lambda
|
|
3326
|
-
settings(deprecated: -> { Current.tenant.legacy_mode? ? :warn : :raise })
|
|
3327
|
-
end
|
|
3328
|
-
```
|
|
3329
|
-
|
|
3330
|
-
### Class or Module
|
|
3331
|
-
|
|
3332
|
-
```ruby
|
|
3333
|
-
class OutdatedTaskDeprecator
|
|
3334
|
-
def call(task)
|
|
3335
|
-
task.class.name.include?("Outdated")
|
|
3336
|
-
end
|
|
3337
|
-
end
|
|
3338
|
-
|
|
3339
|
-
class OutdatedConnector < CMDx::Task
|
|
3340
|
-
# Class or Module
|
|
3341
|
-
settings(deprecated: OutdatedTaskDeprecator)
|
|
3342
|
-
|
|
3343
|
-
# Instance
|
|
3344
|
-
settings(deprecated: OutdatedTaskDeprecator.new)
|
|
3345
|
-
end
|
|
3346
|
-
```
|
|
3347
|
-
|
|
3348
|
-
# Workflows
|
|
3349
|
-
|
|
3350
|
-
Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
|
|
3351
|
-
|
|
3352
|
-
## Declarations
|
|
3353
|
-
|
|
3354
|
-
Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
|
|
3355
|
-
|
|
3356
|
-
!!! warning
|
|
3357
|
-
|
|
3358
|
-
Don't define a `work` method in workflowsβthe module handles execution automatically.
|
|
3359
|
-
|
|
3360
|
-
### Task
|
|
3361
|
-
|
|
3362
|
-
```ruby
|
|
3363
|
-
class OnboardingWorkflow < CMDx::Task
|
|
3364
|
-
include CMDx::Workflow
|
|
3365
|
-
|
|
3366
|
-
task CreateUserProfile
|
|
3367
|
-
task SetupAccountPreferences
|
|
3368
|
-
|
|
3369
|
-
tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
|
|
3370
|
-
end
|
|
3371
|
-
```
|
|
3372
|
-
|
|
3373
|
-
!!! tip
|
|
3374
|
-
|
|
3375
|
-
Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
|
|
3376
|
-
|
|
3377
|
-
### Group
|
|
3378
|
-
|
|
3379
|
-
Group related tasks to share configuration:
|
|
3380
|
-
|
|
3381
|
-
!!! warning "Important"
|
|
3382
|
-
|
|
3383
|
-
Settings and conditionals apply to all tasks in the group.
|
|
3384
|
-
|
|
3385
|
-
```ruby
|
|
3386
|
-
class ContentModerationWorkflow < CMDx::Task
|
|
3387
|
-
include CMDx::Workflow
|
|
3388
|
-
|
|
3389
|
-
# Screening phase
|
|
3390
|
-
tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
|
|
3391
|
-
|
|
3392
|
-
# Review phase
|
|
3393
|
-
tasks ApplyFilters, ScoreContent, FlagSuspicious
|
|
3394
|
-
|
|
3395
|
-
# Decision phase
|
|
3396
|
-
tasks PublishContent, QueueForReview, NotifyModerators
|
|
3397
|
-
end
|
|
3398
|
-
```
|
|
3399
|
-
|
|
3400
|
-
### Conditionals
|
|
3401
|
-
|
|
3402
|
-
Conditionals support multiple syntaxes for flexible execution control:
|
|
3403
|
-
|
|
3404
|
-
```ruby
|
|
3405
|
-
class ContentAccessCheck
|
|
3406
|
-
def call(task)
|
|
3407
|
-
task.context.user.can?(:publish_content)
|
|
3408
|
-
end
|
|
3409
|
-
end
|
|
3410
|
-
|
|
3411
|
-
class OnboardingWorkflow < CMDx::Task
|
|
3412
|
-
include CMDx::Workflow
|
|
3413
|
-
|
|
3414
|
-
# If and/or Unless
|
|
3415
|
-
task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
|
|
3416
|
-
|
|
3417
|
-
# Proc
|
|
3418
|
-
task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
|
|
3419
|
-
|
|
3420
|
-
# Lambda
|
|
3421
|
-
task SendWelcomeEmail, if: proc { context.features_enabled? }
|
|
3422
|
-
|
|
3423
|
-
# Class or Module
|
|
3424
|
-
task SendWelcomeEmail, unless: ContentAccessCheck
|
|
3425
|
-
|
|
3426
|
-
# Instance
|
|
3427
|
-
task SendWelcomeEmail, if: ContentAccessCheck.new
|
|
3428
|
-
|
|
3429
|
-
# Conditional applies to all tasks of this declaration group
|
|
3430
|
-
tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
|
|
3431
|
-
|
|
3432
|
-
private
|
|
3433
|
-
|
|
3434
|
-
def email_configured?
|
|
3435
|
-
context.user.email_address == true
|
|
3436
|
-
end
|
|
3437
|
-
|
|
3438
|
-
def email_disabled?
|
|
3439
|
-
context.user.communication_preference == :disabled
|
|
3440
|
-
end
|
|
3441
|
-
end
|
|
3442
|
-
```
|
|
3443
|
-
|
|
3444
|
-
## Halt Behavior
|
|
3445
|
-
|
|
3446
|
-
By default, skipped tasks don't stop the workflowβthey're treated as no-ops. Configure breakpoints globally or per-task to customize this behavior.
|
|
3447
|
-
|
|
3448
|
-
```ruby
|
|
3449
|
-
class AnalyticsWorkflow < CMDx::Task
|
|
3450
|
-
include CMDx::Workflow
|
|
3451
|
-
|
|
3452
|
-
task CollectMetrics # If fails β workflow stops
|
|
3453
|
-
task FilterOutliers # If skipped β workflow continues
|
|
3454
|
-
task GenerateDashboard # Only runs if no failures occurred
|
|
3455
|
-
end
|
|
3456
|
-
```
|
|
3457
|
-
|
|
3458
|
-
### Task Configuration
|
|
3459
|
-
|
|
3460
|
-
Configure halt behavior for the entire workflow:
|
|
3461
|
-
|
|
3462
|
-
```ruby
|
|
3463
|
-
class SecurityWorkflow < CMDx::Task
|
|
3464
|
-
include CMDx::Workflow
|
|
3465
|
-
|
|
3466
|
-
# Halt on both failed and skipped results
|
|
3467
|
-
settings(workflow_breakpoints: ["skipped", "failed"])
|
|
3468
|
-
|
|
3469
|
-
task PerformSecurityScan
|
|
3470
|
-
task ValidateSecurityRules
|
|
3471
|
-
end
|
|
3472
|
-
|
|
3473
|
-
class OptionalTasksWorkflow < CMDx::Task
|
|
3474
|
-
include CMDx::Workflow
|
|
3475
|
-
|
|
3476
|
-
# Never halt, always continue
|
|
3477
|
-
settings(breakpoints: [])
|
|
3478
|
-
|
|
3479
|
-
task TryBackupData
|
|
3480
|
-
task TryCleanupLogs
|
|
3481
|
-
task TryOptimizeCache
|
|
3482
|
-
end
|
|
3483
|
-
```
|
|
3484
|
-
|
|
3485
|
-
### Group Configuration
|
|
3486
|
-
|
|
3487
|
-
Different task groups can have different halt behavior:
|
|
3488
|
-
|
|
3489
|
-
```ruby
|
|
3490
|
-
class SubscriptionWorkflow < CMDx::Task
|
|
3491
|
-
include CMDx::Workflow
|
|
3492
|
-
|
|
3493
|
-
task CreateSubscription, ValidatePayment, workflow_breakpoints: ["skipped", "failed"]
|
|
3494
|
-
|
|
3495
|
-
# Never halt, always continue
|
|
3496
|
-
task SendConfirmationEmail, UpdateBilling, breakpoints: []
|
|
3497
|
-
end
|
|
3498
|
-
```
|
|
3499
|
-
|
|
3500
|
-
## Nested Workflows
|
|
3501
|
-
|
|
3502
|
-
Build hierarchical workflows by composing workflows within workflows:
|
|
3503
|
-
|
|
3504
|
-
```ruby
|
|
3505
|
-
class EmailPreparationWorkflow < CMDx::Task
|
|
3506
|
-
include CMDx::Workflow
|
|
3507
|
-
|
|
3508
|
-
task ValidateRecipients
|
|
3509
|
-
task CompileTemplate
|
|
3510
|
-
end
|
|
3511
|
-
|
|
3512
|
-
class EmailDeliveryWorkflow < CMDx::Task
|
|
3513
|
-
include CMDx::Workflow
|
|
3514
|
-
|
|
3515
|
-
tasks SendEmails, TrackDeliveries
|
|
3516
|
-
end
|
|
3517
|
-
|
|
3518
|
-
class CompleteEmailWorkflow < CMDx::Task
|
|
3519
|
-
include CMDx::Workflow
|
|
3520
|
-
|
|
3521
|
-
task EmailPreparationWorkflow
|
|
3522
|
-
task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
|
|
3523
|
-
task GenerateDeliveryReport
|
|
3524
|
-
end
|
|
3525
|
-
```
|
|
3526
|
-
|
|
3527
|
-
## Parallel Execution
|
|
3528
|
-
|
|
3529
|
-
Run tasks concurrently using the [Parallel](https://github.com/grosser/parallel) gem. It automatically uses all available processors for maximum throughput.
|
|
3530
|
-
|
|
3531
|
-
!!! warning
|
|
3532
|
-
|
|
3533
|
-
Context is read-only during parallel execution. Load all required data beforehand.
|
|
3534
|
-
|
|
3535
|
-
```ruby
|
|
3536
|
-
class SendWelcomeNotifications < CMDx::Task
|
|
3537
|
-
include CMDx::Workflow
|
|
3538
|
-
|
|
3539
|
-
# Default options (dynamically calculated to available processors)
|
|
3540
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel
|
|
3541
|
-
|
|
3542
|
-
# Fix number of threads
|
|
3543
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_threads: 2
|
|
3544
|
-
|
|
3545
|
-
# Fix number of forked processes
|
|
3546
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_processes: 2
|
|
3547
|
-
|
|
3548
|
-
# NOTE: Reactors are not supported
|
|
3549
|
-
end
|
|
3550
|
-
```
|
|
3551
|
-
|
|
3552
|
-
## Task Generator
|
|
3553
|
-
|
|
3554
|
-
Generate new CMDx workflow tasks quickly using the built-in generator:
|
|
3555
|
-
|
|
3556
|
-
```bash
|
|
3557
|
-
rails generate cmdx:workflow SendNotifications
|
|
3558
|
-
```
|
|
3559
|
-
|
|
3560
|
-
This creates a new workflow task file with the basic structure:
|
|
3561
|
-
|
|
3562
|
-
```ruby
|
|
3563
|
-
# app/tasks/send_notifications.rb
|
|
3564
|
-
class SendNotifications < CMDx::Task
|
|
3565
|
-
include CMDx::Workflow
|
|
3566
|
-
|
|
3567
|
-
tasks Task1, Task2
|
|
3568
|
-
end
|
|
3569
|
-
```
|
|
3570
|
-
|
|
3571
|
-
!!! tip
|
|
3572
|
-
|
|
3573
|
-
Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
|
|
3574
|
-
|
|
3575
|
-
# Tips and Tricks
|
|
3576
|
-
|
|
3577
|
-
Best practices, patterns, and techniques to build maintainable CMDx applications.
|
|
3578
|
-
|
|
3579
|
-
## Project Organization
|
|
3580
|
-
|
|
3581
|
-
### Directory Structure
|
|
3582
|
-
|
|
3583
|
-
Create a well-organized command structure for maintainable applications:
|
|
3584
|
-
|
|
3585
|
-
```text
|
|
3586
|
-
/app/
|
|
3587
|
-
βββ /tasks/
|
|
3588
|
-
βββ /invoices/
|
|
3589
|
-
β βββ calculate_tax.rb
|
|
3590
|
-
β βββ validate_invoice.rb
|
|
3591
|
-
β βββ send_invoice.rb
|
|
3592
|
-
β βββ process_invoice.rb # workflow
|
|
3593
|
-
βββ /reports/
|
|
3594
|
-
β βββ generate_pdf.rb
|
|
3595
|
-
β βββ compile_data.rb
|
|
3596
|
-
β βββ export_csv.rb
|
|
3597
|
-
β βββ create_reports.rb # workflow
|
|
3598
|
-
βββ application_task.rb # base class
|
|
3599
|
-
βββ authenticate_session.rb
|
|
3600
|
-
βββ activate_account.rb
|
|
3601
|
-
```
|
|
3602
|
-
|
|
3603
|
-
### Naming Conventions
|
|
3604
|
-
|
|
3605
|
-
Follow consistent naming patterns for clarity and maintainability:
|
|
3606
|
-
|
|
3607
|
-
```ruby
|
|
3608
|
-
# Verb + Noun
|
|
3609
|
-
class ExportData < CMDx::Task; end
|
|
3610
|
-
class CompressFile < CMDx::Task; end
|
|
3611
|
-
class ValidateSchema < CMDx::Task; end
|
|
3612
|
-
|
|
3613
|
-
# Use present tense verbs for actions
|
|
3614
|
-
class GenerateToken < CMDx::Task; end # β Good
|
|
3615
|
-
class GeneratingToken < CMDx::Task; end # β Avoid
|
|
3616
|
-
class TokenGeneration < CMDx::Task; end # β Avoid
|
|
3617
|
-
```
|
|
3618
|
-
|
|
3619
|
-
### Story Telling
|
|
3620
|
-
|
|
3621
|
-
Break down complex logic into descriptive methods that read like a narrative:
|
|
3622
|
-
|
|
3623
|
-
```ruby
|
|
3624
|
-
class ProcessOrder < CMDx::Task
|
|
3625
|
-
def work
|
|
3626
|
-
charge_payment_method
|
|
3627
|
-
assign_to_warehouse
|
|
3628
|
-
send_notification
|
|
3629
|
-
end
|
|
3630
|
-
|
|
3631
|
-
private
|
|
3632
|
-
|
|
3633
|
-
def charge_payment_method
|
|
3634
|
-
order.primary_payment_method.charge!
|
|
3635
|
-
end
|
|
3636
|
-
|
|
3637
|
-
def assign_to_warehouse
|
|
3638
|
-
order.ready_for_shipping!
|
|
3639
|
-
end
|
|
3640
|
-
|
|
3641
|
-
def send_notification
|
|
3642
|
-
if order.products_out_of_stock?
|
|
3643
|
-
OrderMailer.pending(order).deliver
|
|
3644
|
-
else
|
|
3645
|
-
OrderMailer.preparing(order).deliver
|
|
3646
|
-
end
|
|
3647
|
-
end
|
|
3648
|
-
end
|
|
3649
|
-
```
|
|
3650
|
-
|
|
3651
|
-
### Style Guide
|
|
3652
|
-
|
|
3653
|
-
Follow this order for consistent, readable tasks:
|
|
3654
|
-
|
|
3655
|
-
```ruby
|
|
3656
|
-
class ExportReport < CMDx::Task
|
|
3657
|
-
|
|
3658
|
-
# 1. Register functions
|
|
3659
|
-
register :middleware, CMDx::Middlewares::Correlate
|
|
3660
|
-
register :validator, :format, FormatValidator
|
|
3661
|
-
|
|
3662
|
-
# 2. Define callbacks
|
|
3663
|
-
before_execution :find_report
|
|
3664
|
-
on_complete :track_export_metrics, if: ->(task) { Current.tenant.analytics? }
|
|
3665
|
-
|
|
3666
|
-
# 3. Declare attributes
|
|
3667
|
-
attributes :user_id
|
|
3668
|
-
required :report_id
|
|
3669
|
-
optional :format_type
|
|
3670
|
-
|
|
3671
|
-
# 4. Define work method
|
|
3672
|
-
def work
|
|
3673
|
-
report.compile!
|
|
3674
|
-
report.export!
|
|
3675
|
-
|
|
3676
|
-
context.exported_at = Time.now
|
|
3677
|
-
end
|
|
3678
|
-
|
|
3679
|
-
# TIP: Favor private business logic to reduce the surface of the public API.
|
|
3680
|
-
private
|
|
3681
|
-
|
|
3682
|
-
# 5. Build helper functions
|
|
3683
|
-
def find_report
|
|
3684
|
-
@report ||= Report.find(report_id)
|
|
3685
|
-
end
|
|
3686
|
-
|
|
3687
|
-
def track_export_metrics
|
|
3688
|
-
Analytics.increment(:report_exported)
|
|
3689
|
-
end
|
|
3690
|
-
|
|
3691
|
-
end
|
|
3692
|
-
```
|
|
3693
|
-
|
|
3694
|
-
## Attribute Options
|
|
3695
|
-
|
|
3696
|
-
Use `with_options` to reduce duplication:
|
|
3697
|
-
|
|
3698
|
-
```ruby
|
|
3699
|
-
class ConfigureCompany < CMDx::Task
|
|
3700
|
-
# Apply common options to multiple attributes
|
|
3701
|
-
with_options(type: :string, presence: true) do
|
|
3702
|
-
attributes :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
|
|
3703
|
-
required :company_name, :industry
|
|
3704
|
-
optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
|
|
3705
|
-
end
|
|
3706
|
-
|
|
3707
|
-
# Nested attributes with shared prefix
|
|
3708
|
-
required :headquarters do
|
|
3709
|
-
with_options(prefix: :hq_) do
|
|
3710
|
-
attributes :street, :city, :zip_code, type: :string
|
|
3711
|
-
required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
|
|
3712
|
-
optional :region, type: :string
|
|
3713
|
-
end
|
|
3714
|
-
end
|
|
3715
|
-
|
|
3716
|
-
def work
|
|
3717
|
-
# Your logic here...
|
|
3718
|
-
end
|
|
3719
|
-
end
|
|
3720
|
-
```
|
|
3721
|
-
|
|
3722
|
-
## More Examples
|
|
3723
|
-
|
|
3724
|
-
- [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
|
|
3725
|
-
- [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
|
|
3726
|
-
- [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
|
|
3727
|
-
|