Created
April 14, 2020 19:00
-
-
Save wojtha/6f5c269d879c76ee7e7167abf171da62 to your computer and use it in GitHub Desktop.
Collection of background jobs retry strategies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This module collects various background job retry schedule strategies including the helper functions for the rendering | |
# of the retry table schedule to give better idea in which time frame the jobs are going to be run again. | |
# | |
# Inspired by https://github.com/isaacseymour/activejob-retry and | |
# https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry | |
# | |
module JobRetry | |
module_function | |
# Builtin default Sidekiq retry strategy. | |
# | |
# @see https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry | |
# | |
# Approximate waiting times for given attempt for the builtin Sidekiq retry logic. | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 40s | 0d 00h 00m 40s | |
# 1 | 0d 00h 01m 08s | 0d 00h 01m 48s | |
# 2 | 0d 00h 01m 55s | 0d 00h 03m 43s | |
# 3 | 0d 00h 02m 40s | 0d 00h 06m 23s | |
# 4 | 0d 00h 05m 11s | 0d 00h 11m 34s | |
# 5 | 0d 00h 10m 46s | 0d 00h 22m 20s | |
# 6 | 0d 00h 24m 25s | 0d 00h 46m 45s | |
# 7 | 0d 00h 42m 56s | 0d 01h 29m 41s | |
# 8 | 0d 01h 12m 07s | 0d 02h 41m 48s | |
# 9 | 0d 01h 53m 16s | 0d 04h 35m 04s | |
# 10 | 0d 02h 49m 40s | 0d 07h 24m 44s | |
# 11 | 0d 04h 07m 16s | 0d 11h 32m 00s | |
# 12 | 0d 05h 52m 08s | 0d 17h 24m 08s | |
# 13 | 0d 08h 02m 48s | 1d 01h 26m 56s | |
# 14 | 0d 10h 46m 16s | 1d 12h 13m 12s | |
# 15 | 0d 14h 10m 40s | 2d 02h 23m 52s | |
# 16 | 0d 18h 19m 19s | 2d 20h 43m 11s | |
# 17 | 0d 23h 12m 16s | 3d 19h 55m 27s | |
# 18 | 1d 05h 12m 42s | 5d 01h 08m 09s | |
# 19 | 1d 12h 18m 36s | 6d 13h 26m 45s | |
# 20 | 1d 20h 33m 34s | 8d 10h 00m 19s | |
# 21 | 2d 06h 04m 10s | 10d 16h 04m 29s | |
# 22 | 2d 17h 11m 02s | 13d 09h 15m 31s | |
# 23 | 3d 05h 52m 40s | 16d 15h 08m 11s | |
# 24 | 3d 20h 21m 06s | 20d 11h 29m 17s | |
# 25 | 4d 12h 39m 20s | 25d 00h 08m 37s | |
def sidekiq_exponential_backoff(attempt) | |
(attempt**4) + 15 + (rand(30) * (attempt + 1)) | |
end | |
# Builtin default Delayed::Job retry strategy. | |
# | |
# @see {Delayed::Backend::Base#reschedule_at} | |
# | |
# Approximate waiting times for given attempt for the builtin Delayed::Job retry logic. | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 05s | 0d 00h 00m 05s | |
# 1 | 0d 00h 00m 06s | 0d 00h 00m 11s | |
# 2 | 0d 00h 00m 21s | 0d 00h 00m 32s | |
# 3 | 0d 00h 01m 26s | 0d 00h 01m 58s | |
# 4 | 0d 00h 04m 21s | 0d 00h 06m 19s | |
# 5 | 0d 00h 10m 30s | 0d 00h 16m 49s | |
# 6 | 0d 00h 21m 41s | 0d 00h 38m 30s | |
# 7 | 0d 00h 40m 06s | 0d 01h 18m 36s | |
# 8 | 0d 01h 08m 21s | 0d 02h 26m 57s | |
# 9 | 0d 01h 49m 26s | 0d 04h 16m 23s | |
# 10 | 0d 02h 46m 45s | 0d 07h 03m 08s | |
# 11 | 0d 04h 04m 06s | 0d 11h 07m 14s | |
# 12 | 0d 05h 45m 41s | 0d 16h 52m 55s | |
# 13 | 0d 07h 56m 06s | 1d 00h 49m 01s | |
# 14 | 0d 10h 40m 21s | 1d 11h 29m 22s | |
# 15 | 0d 14h 03m 50s | 2d 01h 33m 12s | |
# 16 | 0d 18h 12m 21s | 2d 19h 45m 33s | |
# 17 | 0d 23h 12m 06s | 3d 18h 57m 39s | |
# 18 | 1d 05h 09m 41s | 5d 00h 07m 20s | |
# 19 | 1d 12h 12m 06s | 6d 12h 19m 26s | |
# 20 | 1d 20h 26m 45s | 8d 08h 46m 11s | |
# 21 | 2d 06h 01m 26s | 10d 14h 47m 37s | |
# 22 | 2d 17h 04m 21s | 13d 07h 51m 58s | |
# 23 | 3d 05h 44m 06s | 16d 13h 36m 04s | |
# 24 | 3d 20h 09m 41s | 20d 09h 45m 45s | |
# 25 | 4d 12h 30m 30s | 24d 22h 16m 15s | |
def delayed_job_exponential_backoff(attempt) | |
(attempt**4) + 5 | |
end | |
# Generic exponential retry strategy based on Sidekiq and Delayed::Job. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 15s | 0d 00h 00m 15s | |
# 1 | 0d 00h 00m 16s | 0d 00h 00m 31s | |
# 2 | 0d 00h 00m 31s | 0d 00h 01m 02s | |
# 3 | 0d 00h 01m 36s | 0d 00h 02m 38s | |
# 4 | 0d 00h 04m 31s | 0d 00h 07m 09s | |
# 5 | 0d 00h 10m 40s | 0d 00h 17m 49s | |
# 6 | 0d 00h 21m 51s | 0d 00h 39m 40s | |
# 7 | 0d 00h 40m 16s | 0d 01h 19m 56s | |
# 8 | 0d 01h 08m 31s | 0d 02h 28m 27s | |
# 9 | 0d 01h 49m 36s | 0d 04h 18m 03s | |
# 10 | 0d 02h 46m 55s | 0d 07h 04m 58s | |
# 11 | 0d 04h 04m 16s | 0d 11h 09m 14s | |
# 12 | 0d 05h 45m 51s | 0d 16h 55m 05s | |
# 13 | 0d 07h 56m 16s | 1d 00h 51m 21s | |
# 14 | 0d 10h 40m 31s | 1d 11h 31m 52s | |
# 15 | 0d 14h 04m 00s | 2d 01h 35m 52s | |
# 16 | 0d 18h 12m 31s | 2d 19h 48m 23s | |
# 17 | 0d 23h 12m 16s | 3d 19h 00m 39s | |
# 18 | 1d 05h 09m 51s | 5d 00h 10m 30s | |
# 19 | 1d 12h 12m 16s | 6d 12h 22m 46s | |
# 20 | 1d 20h 26m 55s | 8d 08h 49m 41s | |
# 21 | 2d 06h 01m 36s | 10d 14h 51m 17s | |
# 22 | 2d 17h 04m 31s | 13d 07h 55m 48s | |
# 23 | 3d 05h 44m 16s | 16d 13h 40m 04s | |
# 24 | 3d 20h 09m 51s | 20d 09h 49m 55s | |
# 25 | 4d 12h 30m 40s | 24d 22h 20m 35s | |
def exponential_backoff(attempt, base: 0, exp: 4, delay: 15) | |
delay + (base + attempt)**exp | |
end | |
# Generic randomized exponential retry strategy based on Sidekiq and Delayed::Job. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 26s | 0d 00h 00m 26s | |
# 1 | 0d 00h 00m 38s | 0d 00h 01m 04s | |
# 2 | 0d 00h 00m 52s | 0d 00h 01m 56s | |
# 3 | 0d 00h 01m 52s | 0d 00h 03m 48s | |
# 4 | 0d 00h 06m 31s | 0d 00h 10m 19s | |
# 5 | 0d 00h 12m 16s | 0d 00h 22m 35s | |
# 6 | 0d 00h 21m 51s | 0d 00h 44m 26s | |
# 7 | 0d 00h 44m 00s | 0d 01h 28m 26s | |
# 8 | 0d 01h 10m 10s | 0d 02h 38m 36s | |
# 9 | 0d 01h 53m 56s | 0d 04h 32m 32s | |
# 10 | 0d 02h 50m 57s | 0d 07h 23m 29s | |
# 11 | 0d 04h 08m 16s | 0d 11h 31m 45s | |
# 12 | 0d 05h 49m 58s | 0d 17h 21m 43s | |
# 13 | 0d 07h 58m 08s | 1d 01h 19m 51s | |
# 14 | 0d 10h 41m 16s | 1d 12h 01m 07s | |
# 15 | 0d 14h 04m 32s | 2d 02h 05m 39s | |
# 16 | 0d 18h 15m 04s | 2d 20h 20m 43s | |
# 17 | 0d 23h 20m 40s | 3d 19h 41m 23s | |
# 18 | 1d 05h 13m 39s | 5d 00h 55m 02s | |
# 19 | 1d 12h 18m 56s | 6d 13h 13m 58s | |
# 20 | 1d 20h 32m 10s | 8d 09h 46m 08s | |
# 21 | 2d 06h 11m 30s | 10d 15h 57m 38s | |
# 22 | 2d 17h 11m 48s | 13d 09h 09m 26s | |
# 23 | 3d 05h 46m 40s | 16d 14h 56m 06s | |
# 24 | 3d 20h 19m 51s | 20d 11h 15m 57s | |
# 25 | 4d 12h 35m 00s | 24d 23h 50m 57s | |
def randomized_exponential_backoff(attempt, base: 0, exp: 4, delay: 15) | |
delay + (base + attempt)**exp + randomize(attempt) | |
end | |
# Generic constant retry strategy. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 15s | 0d 00h 00m 15s | |
# 1 | 0d 00h 00m 15s | 0d 00h 00m 30s | |
# 2 | 0d 00h 00m 15s | 0d 00h 00m 45s | |
# 3 | 0d 00h 00m 15s | 0d 00h 01m 00s | |
# 4 | 0d 00h 00m 15s | 0d 00h 01m 15s | |
# 5 | 0d 00h 00m 15s | 0d 00h 01m 30s | |
# 6 | 0d 00h 00m 15s | 0d 00h 01m 45s | |
# 7 | 0d 00h 00m 15s | 0d 00h 02m 00s | |
# 8 | 0d 00h 00m 15s | 0d 00h 02m 15s | |
# 9 | 0d 00h 00m 15s | 0d 00h 02m 30s | |
def constant_backoff(_attempt, delay: 15) | |
delay | |
end | |
# Generic randomized constant retry strategy. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 19s | 0d 00h 00m 19s | |
# 1 | 0d 00h 00m 33s | 0d 00h 00m 52s | |
# 2 | 0d 00h 00m 44s | 0d 00h 01m 36s | |
# 3 | 0d 00h 00m 27s | 0d 00h 02m 03s | |
# 4 | 0d 00h 00m 35s | 0d 00h 02m 38s | |
# 5 | 0d 00h 00m 33s | 0d 00h 03m 11s | |
# 6 | 0d 00h 00m 35s | 0d 00h 03m 46s | |
# 7 | 0d 00h 00m 19s | 0d 00h 04m 05s | |
# 8 | 0d 00h 00m 22s | 0d 00h 04m 27s | |
# 9 | 0d 00h 00m 30s | 0d 00h 04m 57s | |
def randomized_constant_backoff(_attempt, delay: 15) | |
delay + randomize(0) | |
end | |
# Generic linear retry strategy. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 15s | 0d 00h 00m 15s | |
# 1 | 0d 00h 01m 15s | 0d 00h 01m 30s | |
# 2 | 0d 00h 02m 15s | 0d 00h 03m 45s | |
# 3 | 0d 00h 03m 15s | 0d 00h 07m 00s | |
# 4 | 0d 00h 04m 15s | 0d 00h 11m 15s | |
# 5 | 0d 00h 05m 15s | 0d 00h 16m 30s | |
# 6 | 0d 00h 06m 15s | 0d 00h 22m 45s | |
# 7 | 0d 00h 07m 15s | 0d 00h 30m 00s | |
# 8 | 0d 00h 08m 15s | 0d 00h 38m 15s | |
# 9 | 0d 00h 09m 15s | 0d 00h 47m 30s | |
def linear_backoff(attempt, coefficient: 60, delay: 15) | |
delay + attempt * coefficient | |
end | |
# Generic randomized linear retry strategy. | |
# | |
# Approximate waiting times for given attempt and default values: | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 26s | 0d 00h 00m 26s | |
# 1 | 0d 00h 02m 01s | 0d 00h 02m 27s | |
# 2 | 0d 00h 03m 18s | 0d 00h 05m 45s | |
# 3 | 0d 00h 04m 15s | 0d 00h 10m 00s | |
# 4 | 0d 00h 04m 15s | 0d 00h 14m 15s | |
# 5 | 0d 00h 06m 57s | 0d 00h 21m 12s | |
# 6 | 0d 00h 08m 14s | 0d 00h 29m 26s | |
# 7 | 0d 00h 11m 07s | 0d 00h 40m 33s | |
# 8 | 0d 00h 08m 42s | 0d 00h 49m 15s | |
# 9 | 0d 00h 12m 45s | 0d 01h 02m 00s | |
def randomized_linear_backoff(attempt, coefficient: 60, delay: 15) | |
delay + attempt * coefficient + randomize(attempt) | |
end | |
# Helper method to give a randomize effect to any retry strategy. | |
# | |
# Based on Sidekiq default retry logic: | |
# https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry | |
# https://github.com/mperham/sidekiq/issues/480 | |
# | |
# Maximum waiting times for given attempt when rand = 30: | |
# | |
# # | Next retry backoff | Total waiting time | |
# -------------------------------------------- | |
# 0 | 0d 00h 00m 30s | 0d 00h 00m 30s | |
# 1 | 0d 00h 01m 00s | 0d 00h 01m 30s | |
# 2 | 0d 00h 01m 30s | 0d 00h 03m 00s | |
# 3 | 0d 00h 02m 00s | 0d 00h 05m 00s | |
# 4 | 0d 00h 02m 30s | 0d 00h 07m 30s | |
# 5 | 0d 00h 03m 00s | 0d 00h 10m 30s | |
# 6 | 0d 00h 03m 30s | 0d 00h 14m 00s | |
# 7 | 0d 00h 04m 00s | 0d 00h 18m 00s | |
# 8 | 0d 00h 04m 30s | 0d 00h 22m 30s | |
# 9 | 0d 00h 05m 00s | 0d 00h 27m 30s | |
def randomize(attempt) | |
rand(30) * (attempt + 1) | |
end | |
# Renders table of waiting times for various rescheduling strategies. | |
# | |
# Inspiration taken from https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry. | |
# | |
# @param [Integer] attempts the maximum number of attempts | |
# @param [Integer] width the column width | |
# | |
# @return [String] the rendered table | |
# | |
# @example Usage | |
# puts JobRetry.retry_table(attempts: 25) { |attempt| JobsRetry.linear_backoff(attempt) } | |
# | |
def retry_table(attempts: 25, col_width: 18) | |
out = " # | %#{col_width}s | %#{col_width}s\n" % ['Next retry backoff', 'Total waiting time'] | |
out += "#{'-' * (out.size - 1)}\n" | |
total_wait_time = 0 | |
(0..attempts).each do |attempt| | |
wait_time = yield(attempt) | |
total_wait_time += wait_time | |
out += "%2d | %#{col_width}s | %#{col_width}s\n" % [ | |
attempt, | |
humanize_duration(wait_time), | |
humanize_duration(total_wait_time) | |
] | |
end | |
out | |
end | |
# Utility method to render time duration as human readable string. | |
# | |
# @param [Integer] duration the time duration in seconds | |
# | |
# @return [String] duration as string formatted as "0d 00h 00m 00s" | |
# | |
def humanize_duration(duration) | |
units = %i[day hour minute second] | |
out = {} | |
units.reduce(duration.to_i) do |dur, unit| | |
out[unit], remaining_dur = dur.divmod(1.public_send(unit)) | |
remaining_dur | |
end | |
'%<day>dd %<hour>02dh %<minute>02dm %<second>02ds' % out | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment