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.
@@ -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 it solves:**
5
+ **Common challenges:**
6
6
 
7
7
  - Inconsistent service object patterns across your codebase
8
- - Limited logging makes debugging a nightmare
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
- ```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
- ### Backtraces
24
+ ```sh
25
+ gem install cmdx
82
26
 
83
- Enable detailed backtraces for non-fault exceptions to improve debugging. Optionally clean up stack traces to remove framework noise.
27
+ # - or -
84
28
 
85
- !!! note
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
- ### Exception Handlers
103
-
104
- Register handlers that run when non-fault exceptions occur.
32
+ ## Configuration
105
33
 
106
- !!! tip
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
- ### Middlewares
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
- ### Validators
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
- # Remove validator
233
- config.validators.deregister :username
234
- end
235
- ```
42
+ ## The CERO Pattern
236
43
 
237
- ## Task Configuration
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
- ### Settings
46
+ ### Compose
240
47
 
241
- Override global configuration for specific tasks using `settings`:
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 GenerateInvoice < CMDx::Task
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
- !!! warning "Important"
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
- Register or deregister middlewares, callbacks, coercions, and validators for specific tasks:
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
- class SendCampaignEmail < CMDx::Task
280
- # Middlewares
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
- def work
300
- # Your logic here...
301
- end
302
- end
66
+ # With args
67
+ result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
303
68
  ```
304
69
 
305
- ## Configuration Management
70
+ ### React
306
71
 
307
- ### Access
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
- # Global configuration access
311
- CMDx.configuration.logger #=> <Logger instance>
312
- CMDx.configuration.task_breakpoints #=> ["failed"]
313
- CMDx.configuration.middlewares.registry #=> [<Middleware>, ...]
314
-
315
- # Task configuration access
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
- ### Resetting
327
-
328
- !!! warning
329
-
330
- Resetting affects your entire application. Use this primarily in test environments.
84
+ ### Observe
331
85
 
332
- ```ruby
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
- # Verify reset
337
- CMDx.configuration.task_breakpoints #=> ["failed"] (default)
338
- CMDx.configuration.middlewares.registry #=> Empty registry
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
- # Commonly used in test setup (RSpec example)
341
- RSpec.configure do |config|
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
+ ```
@@ -9,34 +9,34 @@
9
9
  --md-accent-fg-color--transparent: hsla(#{hex2hsl(#fe1817)}, 0.1);
10
10
  }
11
11
 
12
- /* Atom One Light Pro syntax highlighting */
12
+ /* GitHub High Contrast Light syntax highlighting */
13
13
  [data-md-color-scheme="default"] {
14
- --md-code-hl-color: #2c3036;
15
- --md-code-hl-keyword-color: #a626a4;
16
- --md-code-hl-string-color: #50a14f;
17
- --md-code-hl-name-color: #e4564a;
18
- --md-code-hl-function-color: #4078f2;
19
- --md-code-hl-number-color: #ca7601;
20
- --md-code-hl-constant-color: #c18401;
21
- --md-code-hl-comment-color: #9ca0a4;
22
- --md-code-hl-operator-color: #0184bc;
23
- --md-code-hl-punctuation-color:#383a42;
24
- --md-code-hl-variable-color: #e4564a;
25
- --md-code-hl-generic-color: #e4564a;
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
- /* Atom One Dark Pro syntax highlighting */
28
+ /* GitHub High Contrast Dark syntax highlighting */
29
29
  [data-md-color-scheme="slate"] {
30
- --md-code-hl-color: #e5e5e6;
31
- --md-code-hl-keyword-color: #c678dd;
32
- --md-code-hl-string-color: #98c379;
33
- --md-code-hl-name-color: #e06c75;
34
- --md-code-hl-function-color: #61afef;
35
- --md-code-hl-number-color: #d19a66;
36
- --md-code-hl-constant-color: #d19a66;
37
- --md-code-hl-comment-color: #7f848e;
38
- --md-code-hl-operator-color: #56b6c2;
39
- --md-code-hl-punctuation-color:#abb2bf;
40
- --md-code-hl-variable-color: #e06c75;
41
- --md-code-hl-generic-color: #e06c75;
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
  }
@@ -145,7 +145,9 @@ class ConfigureCompany < CMDx::Task
145
145
  end
146
146
  ```
147
147
 
148
- ## Advanced Examples
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
+ ```