cmdx 1.9.1 → 1.10.0

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.
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Build business logic that’s powerful, predictable, and maintainable.
8
8
 
9
- [Documentation](https://drexed.github.io/cmdx) · [Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx/issues) · [Request Feature](https://github.com/drexed/cmdx/issues)
9
+ [Documentation](https://drexed.github.io/cmdx) · [Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx/issues) · [Request Feature](https://github.com/drexed/cmdx/issues) · [LLM.md](https://raw.githubusercontent.com/drexed/cmdx/refs/heads/main/LLM.md)
10
10
 
11
11
  <img alt="Version" src="https://img.shields.io/gem/v/cmdx">
12
12
  <img alt="Build" src="https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg">
data/docs/basics/setup.md CHANGED
@@ -24,6 +24,22 @@ end
24
24
  IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
25
25
  ```
26
26
 
27
+ ## Rollback
28
+
29
+ Undo any operations linked to the given status, helping to restore a pristine state.
30
+
31
+ ```ruby
32
+ class ValidateDocument < CMDx::Task
33
+ def work
34
+ # Your logic here...
35
+ end
36
+
37
+ def rollback
38
+ # Your undo logic...
39
+ end
40
+ end
41
+ ```
42
+
27
43
  ## Inheritance
28
44
 
29
45
  Share configuration across tasks using inheritance:
@@ -65,3 +81,4 @@ Tasks follow a predictable execution pattern:
65
81
  | **Execution** | `executing` | `success`/`failed`/`skipped` | `work` method runs |
66
82
  | **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized |
67
83
  | **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable |
84
+ | **Rollback** | `executed` | `failed`/`skipped` | Work undone |
data/docs/callbacks.md CHANGED
@@ -73,7 +73,7 @@ end
73
73
 
74
74
  ### Class or Module
75
75
 
76
- Implement reusable callback logic in dedicated classes:
76
+ Implement reusable callback logic in dedicated modules and classes:
77
77
 
78
78
  ```ruby
79
79
  class BookingConfirmationCallback
@@ -78,6 +78,16 @@ CMDx.configure do |config|
78
78
  end
79
79
  ```
80
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
+
81
91
  ### Backtraces
82
92
 
83
93
  Enable detailed backtraces for non-fault exceptions to improve debugging. Optionally clean up stack traces to remove framework noise.
@@ -258,7 +268,8 @@ class GenerateInvoice < CMDx::Task
258
268
  deprecated: true, # Task deprecations
259
269
  retries: 3, # Non-fault exception retries
260
270
  retry_on: [External::ApiError], # List of exceptions to retry on
261
- retry_jitter: 1 # Space between retry iteration, eg: current retry num + 1
271
+ retry_jitter: 1, # Space between retry iteration, eg: current retry num + 1
272
+ rollback_on: ["failed", "skipped"], # Rollback on override
262
273
  )
263
274
 
264
275
  def work
@@ -269,7 +280,7 @@ end
269
280
 
270
281
  !!! warning "Important"
271
282
 
272
- Retries reuse the same context. By default, all `StandardError` exceptions are retried unless you specify `retry_on`.
283
+ Retries reuse the same context. By default, all `StandardError` exceptions (including faults) are retried unless you specify `retry_on` option for specific matches.
273
284
 
274
285
  ### Registrations
275
286
 
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.
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
+ ```
@@ -145,7 +145,8 @@ class ConfigureCompany < CMDx::Task
145
145
  end
146
146
  ```
147
147
 
148
- ## Advanced Examples
148
+ ## More 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
+ - [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
@@ -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
+ ```
@@ -9,6 +9,9 @@ module CMDx
9
9
  # @rbs DEFAULT_BREAKPOINTS: Array[String]
10
10
  DEFAULT_BREAKPOINTS = %w[failed].freeze
11
11
 
12
+ # @rbs DEFAULT_ROLLPOINTS: Array[String]
13
+ DEFAULT_ROLLPOINTS = %w[failed].freeze
14
+
12
15
  # Returns the middleware registry for task execution.
13
16
  #
14
17
  # @return [MiddlewareRegistry] The middleware registry
@@ -110,6 +113,16 @@ module CMDx
110
113
  # @rbs @exception_handler: (Proc | nil)
111
114
  attr_accessor :exception_handler
112
115
 
116
+ # Returns the statuses that trigger a task execution rollback.
117
+ #
118
+ # @return [Array<String>] Array of status names that trigger rollback
119
+ #
120
+ # @example
121
+ # config.rollback_on = ["failed", "skipped"]
122
+ #
123
+ # @rbs @rollback_on: Array[String]
124
+ attr_accessor :rollback_on
125
+
113
126
  # Initializes a new Configuration instance with default values.
114
127
  #
115
128
  # Creates new registry instances for middlewares, callbacks, coercions, and
@@ -131,6 +144,7 @@ module CMDx
131
144
 
132
145
  @task_breakpoints = DEFAULT_BREAKPOINTS
133
146
  @workflow_breakpoints = DEFAULT_BREAKPOINTS
147
+ @rollback_on = DEFAULT_ROLLPOINTS
134
148
 
135
149
  @backtrace = false
136
150
  @backtrace_cleaner = nil
@@ -162,6 +176,7 @@ module CMDx
162
176
  validators: @validators,
163
177
  task_breakpoints: @task_breakpoints,
164
178
  workflow_breakpoints: @workflow_breakpoints,
179
+ rollback_on: @rollback_on,
165
180
  backtrace: @backtrace,
166
181
  backtrace_cleaner: @backtrace_cleaner,
167
182
  exception_handler: @exception_handler,
data/lib/cmdx/executor.rb CHANGED
@@ -118,15 +118,12 @@ module CMDx
118
118
  #
119
119
  # @return [Boolean] Whether execution should halt
120
120
  #
121
- # @example
122
- # halt_execution?(fault_exception)
123
- #
124
121
  # @rbs (Exception exception) -> bool
125
122
  def halt_execution?(exception)
126
- breakpoints = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints]
127
- breakpoints = Array(breakpoints).map(&:to_s).uniq
123
+ statuses = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints]
124
+ statuses = Array(statuses).map(&:to_s).uniq
128
125
 
129
- breakpoints.include?(exception.result.status)
126
+ statuses.include?(exception.result.status)
130
127
  end
131
128
 
132
129
  # Determines if execution should be retried based on retry configuration.
@@ -135,9 +132,6 @@ module CMDx
135
132
  #
136
133
  # @return [Boolean] Whether execution should be retried
137
134
  #
138
- # @example
139
- # retry_execution?(standard_error)
140
- #
141
135
  # @rbs (Exception exception) -> bool
142
136
  def retry_execution?(exception)
143
137
  available_retries = (task.class.settings[:retries] || 0).to_i
@@ -157,7 +151,18 @@ module CMDx
157
151
  task.to_h.merge!(reason:, remaining_retries:)
158
152
  end
159
153
 
160
- jitter = task.class.settings[:retry_jitter].to_f * current_retries
154
+ jitter = task.class.settings[:retry_jitter]
155
+ jitter =
156
+ if jitter.is_a?(Symbol)
157
+ task.send(jitter, current_retries)
158
+ elsif jitter.is_a?(Proc)
159
+ task.instance_exec(current_retries, &jitter)
160
+ elsif jitter.respond_to?(:call)
161
+ jitter.call(task, current_retries)
162
+ else
163
+ jitter.to_f * current_retries
164
+ end
165
+
161
166
  sleep(jitter) if jitter.positive?
162
167
 
163
168
  true
@@ -169,9 +174,6 @@ module CMDx
169
174
  #
170
175
  # @raise [Exception] The provided exception
171
176
  #
172
- # @example
173
- # raise_exception(standard_error)
174
- #
175
177
  # @rbs (Exception exception) -> void
176
178
  def raise_exception(exception)
177
179
  Chain.clear
@@ -244,7 +246,7 @@ module CMDx
244
246
  invoke_callbacks(:on_bad) if task.result.bad?
245
247
  end
246
248
 
247
- # Finalizes execution by freezing the task and logging results.
249
+ # Finalizes execution by freezing the task, logging results, and rolling back work.
248
250
  #
249
251
  # @rbs () -> Result
250
252
  def finalize_execution!
@@ -253,6 +255,8 @@ module CMDx
253
255
 
254
256
  freeze_execution!
255
257
  clear_chain!
258
+
259
+ rollback_execution!
256
260
  end
257
261
 
258
262
  # Logs the execution result at the configured log level.
@@ -309,5 +313,18 @@ module CMDx
309
313
  Chain.clear
310
314
  end
311
315
 
316
+ # Rolls back the work of a task.
317
+ #
318
+ # @rbs () -> void
319
+ def rollback_execution!
320
+ return unless task.respond_to?(:rollback)
321
+
322
+ statuses = task.class.settings[:rollback_on]
323
+ statuses = Array(statuses).map(&:to_s).uniq
324
+ return unless statuses.include?(task.result.status)
325
+
326
+ task.rollback
327
+ end
328
+
312
329
  end
313
330
  end
data/lib/cmdx/version.rb CHANGED
@@ -5,6 +5,6 @@ module CMDx
5
5
  # @return [String] the version of the CMDx gem
6
6
  #
7
7
  # @rbs return: String
8
- VERSION = "1.9.1"
8
+ VERSION = "1.10.0"
9
9
 
10
10
  end
data/mkdocs.yml CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  site_name: CMDx
4
4
  site_url: https://drexed.github.io/cmdx/
5
- site_description: Build business logic that's powerful, predictable, and chaos-free.
5
+ site_description: Build business logic that's powerful, predictable, and maintainable.
6
6
  site_author: drexed
7
7
  repo_name: drexed/cmdx
8
8
  repo_url: https://github.com/drexed/cmdx
@@ -108,9 +108,11 @@ nav:
108
108
  - Middlewares: middlewares.md
109
109
  - Logging: logging.md
110
110
  - Internationalization: internationalization.md
111
+ - Retries: retries.md
111
112
  - Deprecation: deprecation.md
112
113
  - Workflows: workflows.md
113
114
  - Tips and Tricks: tips_and_tricks.md
115
+ - LLM.md: https://raw.githubusercontent.com/drexed/cmdx/refs/heads/main/LLM.md
114
116
 
115
117
  extra:
116
118
  generator: false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cmdx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan Gomez
@@ -240,11 +240,13 @@ files:
240
240
  - docs/outcomes/result.md
241
241
  - docs/outcomes/states.md
242
242
  - docs/outcomes/statuses.md
243
+ - docs/retries.md
243
244
  - docs/stylesheets/extra.css
244
245
  - docs/tips_and_tricks.md
245
246
  - docs/workflows.md
246
247
  - examples/active_record_query_tagging.md
247
248
  - examples/paper_trail_whatdunnit.md
249
+ - examples/stoplight_circuit_breaker.md
248
250
  - lib/cmdx.rb
249
251
  - lib/cmdx/.DS_Store
250
252
  - lib/cmdx/attribute.rb