Class: SagaForge::RetryPolicy
- Inherits:
-
Object
- Object
- SagaForge::RetryPolicy
- Defined in:
- lib/saga_forge/retry_policy.rb
Overview
A single, unified description of retry behavior shared by every retry site (step failures and compensation failures).
It answers the only two questions a retry site ever asks:
- retryable?(error, attempts) — should this failure be retried?
- backoff_for(attempts) — how long until the next attempt?
attempts is always the 1-based count of attempts made so far, including
the one that just failed (matching Event#attempts). So on the first
failure attempts == 1.
Instance Attribute Summary collapse
-
#base ⇒ Object
readonly
Returns the value of attribute base.
-
#cap ⇒ Object
readonly
Returns the value of attribute cap.
-
#jitter ⇒ Object
readonly
Returns the value of attribute jitter.
-
#max_attempts ⇒ Object
readonly
Returns the value of attribute max_attempts.
-
#retry_on ⇒ Object
readonly
Returns the value of attribute retry_on.
Class Method Summary collapse
-
.compensation_default ⇒ Object
Compensations are the rollback path: giving up partway through a rollback leaves the saga in a half-undone state, which is worse than retrying for a long time.
-
.compose(*policies) ⇒ Object
Build a composite policy from an ordered list of RetryPolicy objects.
- .step_default ⇒ Object
Instance Method Summary collapse
-
#backoff_for(attempts) ⇒ Object
Equal jitter: half the computed delay plus a random portion of the other half.
-
#budget_key ⇒ Object
Stable per-policy identifier derived from the errors this policy declares (its retry_on), not the error thrown.
-
#initialize(max_attempts: 3, base: 1, cap: 30, jitter: true, retry_on: nil) ⇒ RetryPolicy
constructor
A new instance of RetryPolicy.
-
#matches?(error) ⇒ Boolean
Public routing predicate: would this policy handle this error at all? (independent of the attempt cap).
-
#retry_backoff(error, attempts:) ⇒ Object
Single-call decision used by every retry site: the backoff Duration to retry, or nil to stop.
- #retryable?(error, attempts) ⇒ Boolean
Constructor Details
#initialize(max_attempts: 3, base: 1, cap: 30, jitter: true, retry_on: nil) ⇒ RetryPolicy
Returns a new instance of RetryPolicy.
24 25 26 27 28 29 30 |
# File 'lib/saga_forge/retry_policy.rb', line 24 def initialize(max_attempts: 3, base: 1, cap: 30, jitter: true, retry_on: nil) @max_attempts = max_attempts @base = base @cap = cap @jitter = jitter @retry_on = retry_on end |
Instance Attribute Details
#base ⇒ Object (readonly)
Returns the value of attribute base.
15 16 17 |
# File 'lib/saga_forge/retry_policy.rb', line 15 def base @base end |
#cap ⇒ Object (readonly)
Returns the value of attribute cap.
15 16 17 |
# File 'lib/saga_forge/retry_policy.rb', line 15 def cap @cap end |
#jitter ⇒ Object (readonly)
Returns the value of attribute jitter.
15 16 17 |
# File 'lib/saga_forge/retry_policy.rb', line 15 def jitter @jitter end |
#max_attempts ⇒ Object (readonly)
Returns the value of attribute max_attempts.
15 16 17 |
# File 'lib/saga_forge/retry_policy.rb', line 15 def max_attempts @max_attempts end |
#retry_on ⇒ Object (readonly)
Returns the value of attribute retry_on.
15 16 17 |
# File 'lib/saga_forge/retry_policy.rb', line 15 def retry_on @retry_on end |
Class Method Details
.compensation_default ⇒ Object
Compensations are the rollback path: giving up partway through a rollback leaves the saga in a half-undone state, which is worse than retrying for a long time. So compensations get a far more tolerant policy than steps. 10 attempts gives a tolerant window of up to ~8.5 min (≈4 min typical, since equal jitter puts each wait in [d/2, d]) — enough for a DB failover or deploy restart — without retrying forever on a deterministic bug; cap (600s / 10 min) bounds any single backoff and only binds if a caller configures more attempts.
84 85 86 |
# File 'lib/saga_forge/retry_policy.rb', line 84 def self.compensation_default new(max_attempts: 10, base: 1, cap: 600, jitter: true, retry_on: nil) end |
.compose(*policies) ⇒ Object
Build a composite policy from an ordered list of RetryPolicy objects.
89 90 91 |
# File 'lib/saga_forge/retry_policy.rb', line 89 def self.compose(*policies) CompositeRetryPolicy.new(policies) end |
.step_default ⇒ Object
72 73 74 |
# File 'lib/saga_forge/retry_policy.rb', line 72 def self.step_default new(max_attempts: 3, base: 1, cap: 30, jitter: true, retry_on: nil) end |
Instance Method Details
#backoff_for(attempts) ⇒ Object
Equal jitter: half the computed delay plus a random portion of the other half. Computed once at re-enqueue time and never persisted, so the randomness does not affect re-delivery determinism.
39 40 41 42 43 44 |
# File 'lib/saga_forge/retry_policy.rb', line 39 def backoff_for(attempts) exponent = [attempts - 1, 0].max delay = [cap.to_f, base.to_f * (2**exponent)].min delay = (delay / 2) + rand(0.0..(delay / 2)) if jitter delay.seconds end |
#budget_key ⇒ Object
Stable per-policy identifier derived from the errors this policy declares (its retry_on), not the error thrown. Inside a composite this keys the policy's attempt budget (see Event#retry_budgets), so the budget is shared across every class the policy lists (and their subclasses) and is independent of the policy's position — reordering the composite does not reset counts. A catch-all (retry_on: nil) keys "*".
68 69 70 |
# File 'lib/saga_forge/retry_policy.rb', line 68 def budget_key retry_on.nil? ? "*" : retry_on.map(&:name).sort.join(",") end |
#matches?(error) ⇒ Boolean
Public routing predicate: would this policy handle this error at all? (independent of the attempt cap). nil retry_on = any StandardError; [] = nothing; a list = those classes and their subclasses.
49 50 51 |
# File 'lib/saga_forge/retry_policy.rb', line 49 def matches?(error) retryable_error?(error) end |
#retry_backoff(error, attempts:) ⇒ Object
Single-call decision used by every retry site: the backoff Duration to
retry, or nil to stop. A plain policy uses attempts and ignores any
block (the block exists only so a CompositeRetryPolicy can supply a
per-error count — see CompositeRetryPolicy#retry_backoff).
57 58 59 |
# File 'lib/saga_forge/retry_policy.rb', line 57 def retry_backoff(error, attempts:) retryable?(error, attempts) ? backoff_for(attempts) : nil end |
#retryable?(error, attempts) ⇒ Boolean
32 33 34 |
# File 'lib/saga_forge/retry_policy.rb', line 32 def retryable?(error, attempts) within_attempt_cap?(attempts) && retryable_error?(error) end |