Module: Wurk::Sentry::RetryPolicy

Defined in:
lib/wurk/sentry/retry_policy.rb

Overview

Answers one question: "if this job raises right now, is that the end of the road?" Only terminal failures are worth an alert — reporting every attempt turns a single flaky job into 25 Sentry events over ~21 days.

The prediction has to be made ahead of JobRetry, because the server middleware chain runs inside JobRetry#local (see Processor#dispatch): the middleware's rescue fires before the retry layer has touched the payload. So at rescue time retry_count is still the number of retries already performed, and this class re-derives what JobRetry#process_retry is about to conclude:

* `bump_retry_count` sets `retry_count = 0` on the first failure and
`retry_count += 1` thereafter — so the post-bump count is
`retry_count.nil? ? 0 : retry_count + 1`.
* `exhausted?` kills the job once that count reaches `max_attempts`.

Which makes the last attempt the one that arrives with retry_count == max_attempts - 1 (and, for retry: 0 / retry: false, the very first one).

Not predictable from the payload — and therefore reported one attempt late, or not at all: a sidekiq_retry_in block returning :discard or :kill. Those are host decisions made after this point.

Class Method Summary collapse

Class Method Details

.configured_max_retries(config) ⇒ Object

config is whatever the Chain bound to the middleware — a Capsule in a running worker, a Configuration in tests. Only the latter has [].



66
67
68
69
70
71
# File 'lib/wurk/sentry/retry_policy.rb', line 66

def configured_max_retries(config)
  return nil if config.nil?

  inner = config.respond_to?(:config) ? config.config : config
  inner.respond_to?(:[]) ? inner[:max_retries] : nil
end

.max_attempts(retry_option, config) ⇒ Object



58
59
60
61
62
# File 'lib/wurk/sentry/retry_policy.rb', line 58

def max_attempts(retry_option, config)
  return retry_option if retry_option.is_a?(::Integer)

  configured_max_retries(config) || Wurk::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS
end

.next_retry_count(job) ⇒ Object



53
54
55
56
# File 'lib/wurk/sentry/retry_policy.rb', line 53

def next_retry_count(job)
  count = job['retry_count']
  count ? count + 1 : 0
end

.retry_for_elapsed?(job) ⇒ Boolean

Returns:

  • (Boolean)


73
74
75
76
77
78
79
80
# File 'lib/wurk/sentry/retry_policy.rb', line 73

def retry_for_elapsed?(job)
  failed_at = job['failed_at']
  # First failure: JobRetry stamps `failed_at = now` and then finds the
  # budget un-spent, so the job always gets at least one retry.
  return false unless failed_at

  time_for(failed_at) + job['retry_for'] < ::Time.now
end

.retry_option_for(job, instance) ⇒ Object

Mirrors JobRetry#local: a payload with no retry key falls back to the worker class's own sidekiq_options.



45
46
47
48
49
50
51
# File 'lib/wurk/sentry/retry_policy.rb', line 45

def retry_option_for(job, instance)
  value = job['retry']
  return value unless value.nil?

  klass = instance&.class
  klass.respond_to?(:get_sidekiq_options) ? klass.get_sidekiq_options['retry'] : true
end

.terminal?(job, instance = nil, config = nil) ⇒ Boolean

Returns:

  • (Boolean)


33
34
35
36
37
38
39
40
41
# File 'lib/wurk/sentry/retry_policy.rb', line 33

def terminal?(job, instance = nil, config = nil)
  retry_option = retry_option_for(job, instance)
  return true unless retry_option
  # `retry_for` is a wall-clock budget that supersedes the attempt count
  # entirely (JobRetry#exhausted? branches on it before looking at max).
  return retry_for_elapsed?(job) if job['retry_for']

  next_retry_count(job) >= max_attempts(retry_option, config)
end

.time_for(value) ⇒ Object

Wire format matches JobRetry#time_for: Float seconds or Integer millis.



83
84
85
86
87
# File 'lib/wurk/sentry/retry_policy.rb', line 83

def time_for(value)
  return ::Time.at(value) if value.is_a?(::Float)

  ::Time.at(value / 1000, value % 1000, :millisecond)
end