Module: ActiveJob::Temporal::RetryMapper

Defined in:
lib/activejob/temporal/retry_mapper.rb

Overview

Note:

Algorithmic Wait Values If ‘retry_on` uses a Proc or Symbol for `:wait` (e.g., `:exponentially_longer`), it falls back to the configured `default_retry_initial_interval` because Temporal only accepts static numeric intervals. Temporal’s built-in exponential backoff (via ‘backoff_coefficient`) is used instead.

Note:

Multiple retry_on Declarations If a job class has multiple ‘retry_on` declarations, the first matching handler (based on exception type) is used. Handlers are evaluated in reverse order of declaration (last declared = first matched).

Note:

Exponential Backoff Temporal automatically applies exponential backoff using backoff_coefficient. For example, with initial_interval=30s and backoff_coefficient=2.0, retries occur at 30s, 60s, 120s, 240s intervals (exponentially increasing).

Note:

Unlimited Retries Setting attempts: :unlimited translates to maximum_attempts: 0 in Temporal, which means the activity will retry indefinitely until it succeeds or is cancelled. Use this carefully to avoid infinite retry loops.

Note:

Exception Inheritance Exception matching respects inheritance. A retry_on StandardError declaration will match all StandardError subclasses (RuntimeError, ArgumentError, etc.). More specific exceptions should be declared last to take precedence.

Translates ActiveJob retry DSL to Temporal RetryPolicy.

This module introspects a job class’s ‘retry_on` and `discard_on` declarations and converts them into a Temporal-compatible retry policy hash. It handles:

  • Retry intervals (wait durations)

  • Retry attempt limits

  • Non-retryable error types (from discard_on)

The mapper uses Ruby’s internal ‘rescue_handlers` mechanism to extract retry configuration at runtime, ensuring compatibility with ActiveJob’s DSL.

Examples:

Retry policy structure

{
  initial_interval: 30.0,              # seconds (Float)
  backoff_coefficient: 2.0,            # exponential backoff multiplier
  maximum_attempts: 5,                 # max retry count (0 = unlimited)
  non_retryable_error_types: ["MyError::ClassName"]
}

See Also:

Class Method Summary collapse

Class Method Details

.discard_exception?(job_class, exception) ⇒ Boolean

Checks if an exception should be discarded (not retried).

Inspects the job class’s ‘discard_on` declarations to determine if the given exception matches any discard handler. If true, the activity should raise a non-retryable error to stop Temporal from retrying.

Examples:

Check if exception is discardable

class MyJob < ApplicationJob
  discard_on ActiveRecord::RecordNotFound
end
RetryMapper.discard_exception?(MyJob, ActiveRecord::RecordNotFound.new)
# => true

Parameters:

  • job_class (Class)

    ActiveJob class with discard_on declarations

  • exception (Exception)

    Exception instance to check

Returns:

  • (Boolean)

    true if exception should be discarded, false otherwise



157
158
159
# File 'lib/activejob/temporal/retry_mapper.rb', line 157

def discard_exception?(job_class, exception)
  extractor.discard_exception?(job_class, exception)
end

.exception_execution_keys(job_class) ⇒ Object



136
137
138
# File 'lib/activejob/temporal/retry_mapper.rb', line 136

def exception_execution_keys(job_class)
  extractor.retry_handlers(job_class).filter_map { |handler| handler[:exception_execution_key] }.uniq
end

.for(job_class, exception = nil) ⇒ Hash

Note:

Precedence of Multiple retry_on Declarations When a job class has multiple retry_on declarations, ActiveJob’s rescue_handlers are evaluated in reverse order (last declared = first matched). If you provide an exception argument, the first matching handler is used. Without an exception, the first handler in the list is used.

Builds a Temporal retry policy hash from a job class’s retry configuration.

Inspects the job class’s ‘retry_on` declarations and constructs a retry policy. If an exception is provided, selects the matching retry handler; otherwise uses the first retry handler found.

Examples:

Basic retry policy

class MyJob < ApplicationJob
  retry_on StandardError, wait: 5.seconds, attempts: 3
end
RetryMapper.for(MyJob)
# => { initial_interval: 5.0, backoff_coefficient: 2.0, maximum_attempts: 3, ... }

Unlimited retries

class MyJob < ApplicationJob
  retry_on NetworkError, wait: 30.seconds, attempts: :unlimited
end
RetryMapper.for(MyJob)
# => { ..., maximum_attempts: 0 }

Multiple retry_on declarations (precedence)

class MyJob < ApplicationJob
  retry_on StandardError, wait: 10.seconds, attempts: 5
  retry_on Timeout::Error, wait: 1.second, attempts: 10
end
RetryMapper.for(MyJob, Timeout::Error.new)
# => { initial_interval: 1.0, maximum_attempts: 10, ... }

With discard_on (non-retryable errors)

class MyJob < ApplicationJob
  retry_on StandardError, wait: 5.seconds, attempts: 3
  discard_on ActiveRecord::RecordNotFound
end
RetryMapper.for(MyJob)
# => { ..., non_retryable_error_types: ["ActiveRecord::RecordNotFound"] }

Algorithmic wait (falls back to default)

class MyJob < ApplicationJob
  retry_on NetworkError, wait: :exponentially_longer, attempts: 5
end
RetryMapper.for(MyJob)
# => { initial_interval: 30.0, backoff_coefficient: 2.0, maximum_attempts: 5, ... }
# (Uses config.default_retry_initial_interval because :exponentially_longer is not a static value)

Parameters:

  • job_class (Class)

    ActiveJob class with retry_on/discard_on declarations

  • exception (Exception, nil) (defaults to: nil)

    Optional exception to match against handlers

Returns:

  • (Hash)

    Retry policy with keys:

    • :initial_interval [Float] Initial retry delay in seconds

    • :backoff_coefficient [Float] Exponential backoff multiplier

    • :maximum_attempts [Integer] Max retry attempts (0 = unlimited)

    • :non_retryable_error_types [Array<String>] Exception class names to not retry

Raises:

  • (TypeError)

    if attempts value cannot be converted to Integer



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/activejob/temporal/retry_mapper.rb', line 119

def for(job_class, exception = nil)
  config = ActiveJob::Temporal.config
  retry_entries = extractor.retry_handlers(job_class)
  retry_entry = select_retry_entry(job_class, exception, retry_entries)

  {
    initial_interval: interval_from(retry_entry&.fetch(:wait, nil), config),
    backoff_coefficient: config.default_retry_backoff,
    maximum_attempts: maximum_attempts_from(retry_entries, retry_entry, exception, config, job_class),
    non_retryable_error_types: discard_exception_names(job_class)
  }
end

.retry_handler(job_class, exception) ⇒ Object



132
133
134
# File 'lib/activejob/temporal/retry_mapper.rb', line 132

def retry_handler(job_class, exception)
  select_retry_entry(job_class, exception)
end