cmdx 1.9.1 β 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/.cursor/prompts/llms.md +3 -13
- data/.yardopts +7 -0
- data/CHANGELOG.md +19 -0
- data/README.md +1 -1
- data/Rakefile +21 -0
- data/docs/basics/setup.md +17 -0
- data/docs/callbacks.md +1 -1
- data/docs/configuration.md +314 -0
- data/docs/getting_started.md +37 -289
- data/docs/index.md +4 -2
- data/docs/retries.md +121 -0
- data/docs/stylesheets/extra.css +26 -26
- data/docs/tips_and_tricks.md +3 -1
- data/examples/sidekiq_async_execution.md +29 -0
- data/examples/stoplight_circuit_breaker.md +36 -0
- data/lib/cmdx/configuration.rb +15 -0
- data/lib/cmdx/executor.rb +31 -21
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +66 -18
- metadata +20 -2
- data/LLM.md +0 -3674
data/docs/getting_started.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
**Common challenges
|
|
5
|
+
**Common challenges:**
|
|
6
6
|
|
|
7
7
|
- Inconsistent service object patterns across your codebase
|
|
8
|
-
-
|
|
8
|
+
- Black boxes make debugging a nightmare
|
|
9
9
|
- Fragile error handling erodes confidence
|
|
10
10
|
|
|
11
11
|
**What you get:**
|
|
@@ -17,332 +17,80 @@ CMDx is a Ruby framework for building maintainable, observable business logic th
|
|
|
17
17
|
- Attribute validation with type coercions
|
|
18
18
|
- Sensible defaults and developer-friendly APIs
|
|
19
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
20
|
## Installation
|
|
33
21
|
|
|
34
22
|
Add CMDx to your Gemfile:
|
|
35
23
|
|
|
36
|
-
```
|
|
37
|
-
gem
|
|
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
|
-
### Backtraces
|
|
24
|
+
```sh
|
|
25
|
+
gem install cmdx
|
|
82
26
|
|
|
83
|
-
|
|
27
|
+
# - or -
|
|
84
28
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
In Rails environments, `backtrace_cleaner` defaults to `Rails.backtrace_cleaner.clean`.
|
|
88
|
-
|
|
89
|
-
```ruby
|
|
90
|
-
CMDx.configure do |config|
|
|
91
|
-
# Truthy
|
|
92
|
-
config.backtrace = true
|
|
93
|
-
|
|
94
|
-
# Via callable (must respond to `call(backtrace)`)
|
|
95
|
-
config.backtrace_cleaner = AdvanceCleaner.new
|
|
96
|
-
|
|
97
|
-
# Via proc or lambda
|
|
98
|
-
config.backtrace_cleaner = ->(backtrace) { backtrace[0..5] }
|
|
99
|
-
end
|
|
29
|
+
bundle add cmdx
|
|
100
30
|
```
|
|
101
31
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Register handlers that run when non-fault exceptions occur.
|
|
32
|
+
## Configuration
|
|
105
33
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Use exception handlers to send errors to your APM of choice.
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
CMDx.configure do |config|
|
|
112
|
-
# Via callable (must respond to `call(task, exception)`)
|
|
113
|
-
config.exception_handler = NewRelicReporter
|
|
114
|
-
|
|
115
|
-
# Via proc or lambda
|
|
116
|
-
config.exception_handler = proc do |task, exception|
|
|
117
|
-
APMService.report(exception, extra_data: { task: task.name, id: task.id })
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Logging
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
CMDx.configure do |config|
|
|
126
|
-
config.logger = CustomLogger.new($stdout)
|
|
127
|
-
end
|
|
128
|
-
```
|
|
34
|
+
For Rails applications, run the following command to generate a global configuration file in `config/initializers/cmdx.rb`.
|
|
129
35
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
See the [Middlewares](middlewares.md#declarations) docs for task level configurations.
|
|
133
|
-
|
|
134
|
-
```ruby
|
|
135
|
-
CMDx.configure do |config|
|
|
136
|
-
# Via callable (must respond to `call(task, options)`)
|
|
137
|
-
config.middlewares.register CMDx::Middlewares::Timeout
|
|
138
|
-
|
|
139
|
-
# Via proc or lambda
|
|
140
|
-
config.middlewares.register proc { |task, options|
|
|
141
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
142
|
-
result = yield
|
|
143
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
-
Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" }
|
|
145
|
-
result
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
# With options
|
|
149
|
-
config.middlewares.register AuditTrailMiddleware, service_name: "document_processor"
|
|
150
|
-
|
|
151
|
-
# Remove middleware
|
|
152
|
-
config.middlewares.deregister CMDx::Middlewares::Timeout
|
|
153
|
-
end
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
!!! note
|
|
157
|
-
|
|
158
|
-
Middlewares are executed in registration order. Each middleware wraps the next, creating an execution chain around task logic.
|
|
159
|
-
|
|
160
|
-
### Callbacks
|
|
161
|
-
|
|
162
|
-
See the [Callbacks](callbacks.md#declarations) docs for task level configurations.
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
CMDx.configure do |config|
|
|
166
|
-
# Via method
|
|
167
|
-
config.callbacks.register :before_execution, :initialize_user_session
|
|
168
|
-
|
|
169
|
-
# Via callable (must respond to `call(task)`)
|
|
170
|
-
config.callbacks.register :on_success, LogUserActivity
|
|
171
|
-
|
|
172
|
-
# Via proc or lambda
|
|
173
|
-
config.callbacks.register :on_complete, proc { |task|
|
|
174
|
-
execution_time = task.metadata[:runtime]
|
|
175
|
-
Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"])
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
# With options
|
|
179
|
-
config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task?
|
|
180
|
-
|
|
181
|
-
# Remove callback
|
|
182
|
-
config.callbacks.deregister :on_success, LogUserActivity
|
|
183
|
-
end
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Coercions
|
|
187
|
-
|
|
188
|
-
See the [Attributes - Coercions](attributes/coercions.md#declarations) docs for task level configurations.
|
|
189
|
-
|
|
190
|
-
```ruby
|
|
191
|
-
CMDx.configure do |config|
|
|
192
|
-
# Via callable (must respond to `call(value, options)`)
|
|
193
|
-
config.coercions.register :currency, CurrencyCoercion
|
|
194
|
-
|
|
195
|
-
# Via method (must match signature `def coordinates_coercion(value, options)`)
|
|
196
|
-
config.coercions.register :coordinates, :coordinates_coercion
|
|
197
|
-
|
|
198
|
-
# Via proc or lambda
|
|
199
|
-
config.coercions.register :tag_list, proc { |value, options|
|
|
200
|
-
delimiter = options[:delimiter] || ','
|
|
201
|
-
max_tags = options[:max_tags] || 50
|
|
202
|
-
|
|
203
|
-
tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?)
|
|
204
|
-
tags.first(max_tags)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
# Remove coercion
|
|
208
|
-
config.coercions.deregister :currency
|
|
209
|
-
end
|
|
36
|
+
```bash
|
|
37
|
+
rails generate cmdx:install
|
|
210
38
|
```
|
|
211
39
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
See the [Attributes - Validations](attributes/validations.md#declarations) docs for task level configurations.
|
|
215
|
-
|
|
216
|
-
```ruby
|
|
217
|
-
CMDx.configure do |config|
|
|
218
|
-
# Via callable (must respond to `call(value, options)`)
|
|
219
|
-
config.validators.register :username, UsernameValidator
|
|
220
|
-
|
|
221
|
-
# Via method (must match signature `def url_validator(value, options)`)
|
|
222
|
-
config.validators.register :url, :url_validator
|
|
223
|
-
|
|
224
|
-
# Via proc or lambda
|
|
225
|
-
config.validators.register :access_token, proc { |value, options|
|
|
226
|
-
expected_prefix = options[:prefix] || "tok_"
|
|
227
|
-
minimum_length = options[:min_length] || 40
|
|
228
|
-
|
|
229
|
-
value.start_with?(expected_prefix) && value.length >= minimum_length
|
|
230
|
-
}
|
|
40
|
+
If not using Rails, manually copy the [configuration file](https://github.com/drexed/cmdx/blob/main/lib/generators/cmdx/templates/install.rb).
|
|
231
41
|
|
|
232
|
-
|
|
233
|
-
config.validators.deregister :username
|
|
234
|
-
end
|
|
235
|
-
```
|
|
42
|
+
## The CERO Pattern
|
|
236
43
|
|
|
237
|
-
|
|
44
|
+
CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") patternβa simple yet powerful approach to building reliable business logic.
|
|
238
45
|
|
|
239
|
-
###
|
|
46
|
+
### Compose
|
|
240
47
|
|
|
241
|
-
|
|
48
|
+
Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
|
|
242
49
|
|
|
243
50
|
```ruby
|
|
244
|
-
class
|
|
245
|
-
settings(
|
|
246
|
-
# Global configuration overrides
|
|
247
|
-
task_breakpoints: ["failed"], # Breakpoint override
|
|
248
|
-
workflow_breakpoints: [], # Breakpoint override
|
|
249
|
-
backtrace: true, # Toggle backtrace
|
|
250
|
-
backtrace_cleaner: ->(bt) { bt[0..5] }, # Backtrace cleaner
|
|
251
|
-
logger: CustomLogger.new($stdout), # Custom logger
|
|
252
|
-
|
|
253
|
-
# Task configuration settings
|
|
254
|
-
breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints
|
|
255
|
-
log_level: :info, # Log level override
|
|
256
|
-
log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
|
|
257
|
-
tags: ["billing", "financial"], # Logging tags
|
|
258
|
-
deprecated: true, # Task deprecations
|
|
259
|
-
retries: 3, # Non-fault exception retries
|
|
260
|
-
retry_on: [External::ApiError], # List of exceptions to retry on
|
|
261
|
-
retry_jitter: 1 # Space between retry iteration, eg: current retry num + 1
|
|
262
|
-
)
|
|
263
|
-
|
|
51
|
+
class AnalyzeMetrics < CMDx::Task
|
|
264
52
|
def work
|
|
265
53
|
# Your logic here...
|
|
266
54
|
end
|
|
267
55
|
end
|
|
268
56
|
```
|
|
269
57
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
Retries reuse the same context. By default, all `StandardError` exceptions are retried unless you specify `retry_on`.
|
|
273
|
-
|
|
274
|
-
### Registrations
|
|
58
|
+
### Execute
|
|
275
59
|
|
|
276
|
-
|
|
60
|
+
Invoke tasks with a consistent API that always returns a result object. Execution automatically handles validation, type coercion, error handling, and logging. Arguments are validated and coerced before your task logic runs.
|
|
277
61
|
|
|
278
62
|
```ruby
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
register :middleware, CMDx::Middlewares::Timeout
|
|
282
|
-
deregister :middleware, AuditTrailMiddleware
|
|
283
|
-
|
|
284
|
-
# Callbacks
|
|
285
|
-
register :callback, :on_complete, proc { |task|
|
|
286
|
-
runtime = task.metadata[:runtime]
|
|
287
|
-
Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"])
|
|
288
|
-
}
|
|
289
|
-
deregister :callback, :before_execution, :initialize_user_session
|
|
290
|
-
|
|
291
|
-
# Coercions
|
|
292
|
-
register :coercion, :currency, CurrencyCoercion
|
|
293
|
-
deregister :coercion, :coordinates
|
|
294
|
-
|
|
295
|
-
# Validators
|
|
296
|
-
register :validator, :username, :username_validator
|
|
297
|
-
deregister :validator, :url
|
|
63
|
+
# Without args
|
|
64
|
+
result = AnalyzeMetrics.execute
|
|
298
65
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
end
|
|
302
|
-
end
|
|
66
|
+
# With args
|
|
67
|
+
result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
|
|
303
68
|
```
|
|
304
69
|
|
|
305
|
-
|
|
70
|
+
### React
|
|
306
71
|
|
|
307
|
-
|
|
72
|
+
Every execution returns a result object with a clear outcome. Check the result's state (`success?`, `failed?`, `skipped?`) and access returned values, error messages, and metadata to make informed decisions.
|
|
308
73
|
|
|
309
74
|
```ruby
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
#
|
|
316
|
-
class ProcessUpload < CMDx::Task
|
|
317
|
-
settings(tags: ["files", "storage"])
|
|
318
|
-
|
|
319
|
-
def work
|
|
320
|
-
self.class.settings[:logger] #=> Global configuration value
|
|
321
|
-
self.class.settings[:tags] #=> Task configuration value => ["files", "storage"]
|
|
322
|
-
end
|
|
75
|
+
if result.success?
|
|
76
|
+
# Handle success
|
|
77
|
+
elsif result.skipped?
|
|
78
|
+
# Handle skipped
|
|
79
|
+
elsif result.failed?
|
|
80
|
+
# Handle failed
|
|
323
81
|
end
|
|
324
82
|
```
|
|
325
83
|
|
|
326
|
-
###
|
|
327
|
-
|
|
328
|
-
!!! warning
|
|
329
|
-
|
|
330
|
-
Resetting affects your entire application. Use this primarily in test environments.
|
|
84
|
+
### Observe
|
|
331
85
|
|
|
332
|
-
|
|
333
|
-
# Reset to framework defaults
|
|
334
|
-
CMDx.reset_configuration!
|
|
86
|
+
Every task execution generates structured logs with execution chains, runtime metrics, and contextual metadata. Logs can be automatically correlated using chain IDs, making it easy to trace complex workflows and debug issues.
|
|
335
87
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
88
|
+
```log
|
|
89
|
+
I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
|
|
90
|
+
index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
|
|
339
91
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
config.before(:each) do
|
|
343
|
-
CMDx.reset_configuration!
|
|
344
|
-
end
|
|
345
|
-
end
|
|
92
|
+
I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
|
|
93
|
+
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
|
346
94
|
```
|
|
347
95
|
|
|
348
96
|
## Task Generator
|
data/docs/index.md
CHANGED
|
@@ -8,7 +8,7 @@ Build business logic that's powerful, predictable, and maintainable.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
Say goodbye to messy service objects. CMDx helps you design business logic with clarity and consistencyβbuild faster, debug easier, and ship with confidence.
|
|
11
|
+
Say goodbye to messy service objects. CMDx (pronounced "Command X") helps you design business logic with clarity and consistencyβbuild faster, debug easier, and ship with confidence.
|
|
12
12
|
|
|
13
13
|
!!! note
|
|
14
14
|
|
|
@@ -24,7 +24,9 @@ CMDx works with any Ruby framework. Rails support is built-in, but it's framewor
|
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
26
|
gem install cmdx
|
|
27
|
+
|
|
27
28
|
# - or -
|
|
29
|
+
|
|
28
30
|
bundle add cmdx
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -125,7 +127,7 @@ For backwards compatibility of certain functionality:
|
|
|
125
127
|
|
|
126
128
|
## Contributing
|
|
127
129
|
|
|
128
|
-
Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
|
|
130
|
+
Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](https://github.com/drexed/cmdx/blob/main/CODE_OF_CONDUCT.md).
|
|
129
131
|
|
|
130
132
|
## License
|
|
131
133
|
|
data/docs/retries.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Retries
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
Configure retries upto n attempts without any delay.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class FetchExternalData < CMDx::Task
|
|
11
|
+
settings retries: 3
|
|
12
|
+
|
|
13
|
+
def work
|
|
14
|
+
response = HTTParty.get("https://api.example.com/data")
|
|
15
|
+
context.data = response.parsed_response
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When an exception occurs during execution, CMDx automatically retries up to the configured limit. Each retry attempt is logged at the `warn` level with retry metadata. If all retries are exhausted, the task fails with the original exception.
|
|
21
|
+
|
|
22
|
+
## Selective Retries
|
|
23
|
+
|
|
24
|
+
By default, CMDx retries on `StandardError` and its subclasses. Narrow this to specific exception types:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class ProcessPayment < CMDx::Task
|
|
28
|
+
settings retries: 5, retry_on: [Stripe::RateLimitError, Net::ReadTimeout]
|
|
29
|
+
|
|
30
|
+
def work
|
|
31
|
+
# Your logic here...
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
!!! warning "Important"
|
|
37
|
+
|
|
38
|
+
Only exceptions matching the `retry_on` configuration will trigger retries. Uncaught exceptions immediately fail the task.
|
|
39
|
+
|
|
40
|
+
## Retry Jitter
|
|
41
|
+
|
|
42
|
+
Add delays between retry attempts to avoid overwhelming external services or to implement exponential backoff strategies.
|
|
43
|
+
|
|
44
|
+
### Fixed Value
|
|
45
|
+
|
|
46
|
+
Use a numeric value to calculate linear delay (`jitter * current_retry`):
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class ImportRecords < CMDx::Task
|
|
50
|
+
settings retries: 3, retry_jitter: 0.5
|
|
51
|
+
|
|
52
|
+
def work
|
|
53
|
+
# Delays: 0s, 0.5s (retry 1), 1.0s (retry 2), 1.5s (retry 3)
|
|
54
|
+
context.records = ExternalAPI.fetch_records
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Symbol References
|
|
60
|
+
|
|
61
|
+
Define an instance method for custom delay logic:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class SyncInventory < CMDx::Task
|
|
65
|
+
settings retries: 5, retry_jitter: :exponential_backoff
|
|
66
|
+
|
|
67
|
+
def work
|
|
68
|
+
context.inventory = InventoryAPI.sync
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def exponential_backoff(current_retry)
|
|
74
|
+
2 ** current_retry # 2s, 4s, 8s, 16s, 32s
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Proc or Lambda
|
|
80
|
+
|
|
81
|
+
Pass a proc for inline delay calculations:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class PollJobStatus < CMDx::Task
|
|
85
|
+
# Proc
|
|
86
|
+
settings retries: 10, retry_jitter: proc { |retry_count| [retry_count * 0.5, 5.0].min }
|
|
87
|
+
|
|
88
|
+
# Lambda
|
|
89
|
+
settings retries: 10, retry_jitter: ->(retry_count) { [retry_count * 0.5, 5.0].min }
|
|
90
|
+
|
|
91
|
+
def work
|
|
92
|
+
# Delays: 0.5s, 1.0s, 1.5s, 2.0s, 2.5s, 3.0s, 3.5s, 4.0s, 4.5s, 5.0s (capped)
|
|
93
|
+
context.status = JobAPI.check_status(context.job_id)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Class or Module
|
|
99
|
+
|
|
100
|
+
Implement reusable delay logic in dedicated modules and classes:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
class ExponentialBackoff
|
|
104
|
+
def call(task, retry_count)
|
|
105
|
+
base_delay = task.context.base_delay || 1.0
|
|
106
|
+
[base_delay * (2 ** retry_count), 60.0].min
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class FetchUserProfile < CMDx::Task
|
|
111
|
+
# Class or Module
|
|
112
|
+
settings retries: 4, retry_jitter: ExponentialBackoff
|
|
113
|
+
|
|
114
|
+
# Instance
|
|
115
|
+
settings retries: 4, retry_jitter: ExponentialBackoff.new
|
|
116
|
+
|
|
117
|
+
def work
|
|
118
|
+
# Your logic here...
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
data/docs/stylesheets/extra.css
CHANGED
|
@@ -9,34 +9,34 @@
|
|
|
9
9
|
--md-accent-fg-color--transparent: hsla(#{hex2hsl(#fe1817)}, 0.1);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/*
|
|
12
|
+
/* GitHub High Contrast Light syntax highlighting */
|
|
13
13
|
[data-md-color-scheme="default"] {
|
|
14
|
-
--md-code-hl-color: #
|
|
15
|
-
--md-code-hl-keyword-color: #
|
|
16
|
-
--md-code-hl-string-color: #
|
|
17
|
-
--md-code-hl-name-color: #
|
|
18
|
-
--md-code-hl-function-color: #
|
|
19
|
-
--md-code-hl-number-color: #
|
|
20
|
-
--md-code-hl-constant-color: #
|
|
21
|
-
--md-code-hl-comment-color: #
|
|
22
|
-
--md-code-hl-operator-color: #
|
|
23
|
-
--md-code-hl-punctuation-color:#
|
|
24
|
-
--md-code-hl-variable-color: #
|
|
25
|
-
--md-code-hl-generic-color: #
|
|
14
|
+
--md-code-hl-color: #0e1116;
|
|
15
|
+
--md-code-hl-keyword-color: #a0095d;
|
|
16
|
+
--md-code-hl-string-color: #024c1a;
|
|
17
|
+
--md-code-hl-name-color: #622cbc;
|
|
18
|
+
--md-code-hl-function-color: #622cbc;
|
|
19
|
+
--md-code-hl-number-color: #0349b4;
|
|
20
|
+
--md-code-hl-constant-color: #702c00;
|
|
21
|
+
--md-code-hl-comment-color: #66707b;
|
|
22
|
+
--md-code-hl-operator-color: #a0095d;
|
|
23
|
+
--md-code-hl-punctuation-color:#0e1116;
|
|
24
|
+
--md-code-hl-variable-color: #702c00;
|
|
25
|
+
--md-code-hl-generic-color: #622cbc;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/*
|
|
28
|
+
/* GitHub High Contrast Dark syntax highlighting */
|
|
29
29
|
[data-md-color-scheme="slate"] {
|
|
30
|
-
--md-code-hl-color: #
|
|
31
|
-
--md-code-hl-keyword-color: #
|
|
32
|
-
--md-code-hl-string-color: #
|
|
33
|
-
--md-code-hl-name-color: #
|
|
34
|
-
--md-code-hl-function-color: #
|
|
35
|
-
--md-code-hl-number-color: #
|
|
36
|
-
--md-code-hl-constant-color: #
|
|
37
|
-
--md-code-hl-comment-color: #
|
|
38
|
-
--md-code-hl-operator-color: #
|
|
39
|
-
--md-code-hl-punctuation-color:#
|
|
40
|
-
--md-code-hl-variable-color: #
|
|
41
|
-
--md-code-hl-generic-color: #
|
|
30
|
+
--md-code-hl-color: #f0f3f6;
|
|
31
|
+
--md-code-hl-keyword-color: #ff9492;
|
|
32
|
+
--md-code-hl-string-color: #addcff;
|
|
33
|
+
--md-code-hl-name-color: #dbb7ff;
|
|
34
|
+
--md-code-hl-function-color: #dbb7ff;
|
|
35
|
+
--md-code-hl-number-color: #91cbff;
|
|
36
|
+
--md-code-hl-constant-color: #ffb757;
|
|
37
|
+
--md-code-hl-comment-color: #9ea7b3;
|
|
38
|
+
--md-code-hl-operator-color: #ff9492;
|
|
39
|
+
--md-code-hl-punctuation-color:#f0f3f6;
|
|
40
|
+
--md-code-hl-variable-color: #ffb757;
|
|
41
|
+
--md-code-hl-generic-color: #dbb7ff;
|
|
42
42
|
}
|
data/docs/tips_and_tricks.md
CHANGED
|
@@ -145,7 +145,9 @@ class ConfigureCompany < CMDx::Task
|
|
|
145
145
|
end
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
##
|
|
148
|
+
## Useful Examples
|
|
149
149
|
|
|
150
150
|
- [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
|
|
151
151
|
- [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
|
|
152
|
+
- [Sidekiq Async Execution](https://github.com/drexed/cmdx/blob/main/examples/sidekiq_async_execution.md)
|
|
153
|
+
- [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Sidekiq Async Execute
|
|
2
|
+
|
|
3
|
+
Execute tasks asynchronously using Sidekiq without creating separate job classes.
|
|
4
|
+
|
|
5
|
+
<https://github.com/sidekiq/sidekiq>
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class MyTask < CMDx::Task
|
|
11
|
+
include Sidekiq::Job
|
|
12
|
+
|
|
13
|
+
def work
|
|
14
|
+
# Do work...
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Use execute! to trigger Sidekiq's retry logic on failures/exceptions.
|
|
18
|
+
def perform
|
|
19
|
+
self.class.execute!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Usage
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
MyTask.perform_async
|
|
29
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Stoplight Circuit Breaker
|
|
2
|
+
|
|
3
|
+
Integrate circuit breakers to protect external service calls and prevent cascading failures when dependencies are unavailable.
|
|
4
|
+
|
|
5
|
+
<https://github.com/bolshakov/stoplight>
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# lib/cmdx_stoplight_middleware.rb
|
|
11
|
+
class CmdxStoplightMiddleware
|
|
12
|
+
def self.call(task, **options, &)
|
|
13
|
+
light = Stoplight(options[:name] || task.class.name, **options)
|
|
14
|
+
light.run(&)
|
|
15
|
+
rescue Stoplight::Error::RedLight => e
|
|
16
|
+
task.result.tap { |r| r.fail!("[#{e.class}] #{e.message}", cause: e) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Usage
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class MyTask < CMDx::Task
|
|
25
|
+
# With default options
|
|
26
|
+
register :middleware, CmdxStoplightMiddleware
|
|
27
|
+
|
|
28
|
+
# With stoplight options
|
|
29
|
+
register :middleware, CmdxStoplightMiddleware, cool_off_time: 10
|
|
30
|
+
|
|
31
|
+
def work
|
|
32
|
+
# Do work...
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
```
|