Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added
- Added `rollback` ability to undo on matching status
- Added documentation for retries

### Updated
- Added `retry_jitter` option to support symbol, proc, and callables

## [1.9.1] - 2025-10-22

Expand Down
2 changes: 1 addition & 1 deletion docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ end

### Class or Module

Implement reusable callback logic in dedicated classes:
Implement reusable callback logic in dedicated modules and classes:

```ruby
class BookingConfirmationCallback
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ end

!!! warning "Important"

Retries reuse the same context. By default, all `StandardError` exceptions are retried unless you specify `retry_on`.
Retries reuse the same context. By default, all `StandardError` exceptions (including faults) are retried unless you specify `retry_on` option for specific matches.

### Registrations

Expand Down
122 changes: 122 additions & 0 deletions docs/retries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Retries

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.

## Basic Usage

Configure retries upto n attempts without any delay.

```ruby
class FetchExternalData < CMDx::Task
settings retries: 3

def work
response = HTTParty.get("https://api.example.com/data")
context.data = response.parsed_response
end
end
```

When an exception occurs during execution, CMDx automatically retries up to the configured limit.

## Selective Retries

By default, CMDx retries on `StandardError` and its subclasses. Narrow this to specific exception types:

```ruby
class ProcessPayment < CMDx::Task
settings retries: 5, retry_on: [Stripe::RateLimitError, Net::ReadTimeout]

def work
# Your logic here...
end
end
```

!!! warning "Important"

Only exceptions matching the `retry_on` configuration will trigger retries. Uncaught exceptions immediately fail the task.

## Retry Jitter

Add delays between retry attempts to avoid overwhelming external services or to implement exponential backoff strategies.

### Fixed Value

Use a numeric value to calculate linear delay (`jitter * current_retry`):

```ruby
class ImportRecords < CMDx::Task
# Fixed
settings retries: 3, retry_jitter: 0.5

def work
# Delays: 0s, 0.5s (retry 1), 1.0s (retry 2), 1.5s (retry 3)
context.records = ExternalAPI.fetch_records
end
end
```

### Symbol References

Define an instance method for custom delay logic:

```ruby
class SyncInventory < CMDx::Task
settings retries: 5, retry_jitter: :exponential_backoff

def work
context.inventory = InventoryAPI.sync
end

private

def exponential_backoff(current_retry)
2 ** current_retry # 2s, 4s, 8s, 16s, 32s
end
end
```

### Proc or Lambda

Pass a proc for inline delay calculations:

```ruby
class PollJobStatus < CMDx::Task
# Proc
settings retries: 10, retry_jitter: proc { |retry_count| [retry_count * 0.5, 5.0].min }

# Lambda
settings retries: 10, retry_jitter: ->(retry_count) { [retry_count * 0.5, 5.0].min }

def work
# Delays: 0.5s, 1.0s, 1.5s, 2.0s, 2.5s, 3.0s, 3.5s, 4.0s, 4.5s, 5.0s (capped)
context.status = JobAPI.check_status(context.job_id)
end
end
```

### Class or Module

Implement reusable delay logic in dedicated modules and classes:

```ruby
class ExponentialBackoff
def call(task, retry_count)
base_delay = task.context.base_delay || 1.0
[base_delay * (2 ** retry_count), 60.0].min
end
end

class FetchUserProfile < CMDx::Task
# Class or Module
settings retries: 4, retry_jitter: ExponentialBackoff

# Instance
settings retries: 4, retry_jitter: ExponentialBackoff.new

def work
# Your logic here...
end
end
```
13 changes: 12 additions & 1 deletion lib/cmdx/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,18 @@ def retry_execution?(exception)
task.to_h.merge!(reason:, remaining_retries:)
end

jitter = task.class.settings[:retry_jitter].to_f * current_retries
jitter = task.class.settings[:retry_jitter]
jitter =
if jitter.is_a?(Symbol)
task.send(jitter, current_retries)
elsif jitter.is_a?(Proc)
task.instance_exec(current_retries, &jitter)
elsif jitter.respond_to?(:call)
jitter.call(task, current_retries)
else
jitter.to_f * current_retries
end

sleep(jitter) if jitter.positive?

true
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ nav:
- Middlewares: middlewares.md
- Logging: logging.md
- Internationalization: internationalization.md
- Retries: retries.md
- Deprecation: deprecation.md
- Workflows: workflows.md
- Tips and Tricks: tips_and_tricks.md
Expand Down
Loading