2020-01-03 04:07:33 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
#
|
|
|
|
# A concern that helps run exactly one instance of a worker, over and over,
|
|
|
|
# until it returns false or raises.
|
|
|
|
#
|
|
|
|
# To ensure the worker is always up, you can schedule it every minute with
|
|
|
|
# sidekiq-cron. Excess jobs will immediately exit due to an exclusive lease.
|
|
|
|
#
|
|
|
|
# The worker must define:
|
|
|
|
#
|
|
|
|
# - `#perform`
|
|
|
|
# - `#lease_timeout`
|
|
|
|
#
|
|
|
|
# The worker spec should include `it_behaves_like 'reenqueuer'` and
|
2020-10-26 02:08:48 -04:00
|
|
|
# `it_behaves_like '#perform is rate limited to 1 call per'`.
|
2020-01-03 04:07:33 -05:00
|
|
|
#
|
|
|
|
# Optionally override `#minimum_duration` to adjust the rate limit.
|
|
|
|
#
|
|
|
|
# When `#perform` returns false, the job will not be reenqueued. Instead, we
|
|
|
|
# will wait for the next one scheduled by sidekiq-cron.
|
|
|
|
#
|
|
|
|
# #lease_timeout should be longer than the longest possible `#perform`.
|
|
|
|
# The lease is normally released in an ensure block, but it is possible to
|
|
|
|
# orphan the lease by killing Sidekiq, so it should also be as short as
|
|
|
|
# possible. Consider that long-running jobs are generally not recommended.
|
|
|
|
# Ideally, every job finishes within 25 seconds because that is the default
|
|
|
|
# wait time for graceful termination.
|
|
|
|
#
|
|
|
|
# Timing: It runs as often as Sidekiq allows. We rate limit with sleep for
|
|
|
|
# now: https://gitlab.com/gitlab-org/gitlab/issues/121697
|
|
|
|
module Reenqueuer
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
prepended do
|
|
|
|
include ExclusiveLeaseGuard
|
|
|
|
include ReenqueuerSleeper
|
|
|
|
|
|
|
|
sidekiq_options retry: false
|
2020-12-03 22:09:49 -05:00
|
|
|
deduplicate :none
|
2020-01-03 04:07:33 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def perform(*args)
|
2022-05-17 14:09:20 -04:00
|
|
|
set_custom_lease_key(*args) if self.respond_to?(:set_custom_lease_key)
|
|
|
|
|
2020-01-03 04:07:33 -05:00
|
|
|
try_obtain_lease do
|
|
|
|
reenqueue(*args) do
|
|
|
|
ensure_minimum_duration(minimum_duration) do
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def reenqueue(*args)
|
2020-11-24 19:09:24 -05:00
|
|
|
result = yield
|
|
|
|
|
|
|
|
self.class.perform_async(*args) if result
|
|
|
|
|
|
|
|
result
|
2020-01-03 04:07:33 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Override as needed
|
|
|
|
def minimum_duration
|
|
|
|
5.seconds
|
|
|
|
end
|
|
|
|
|
|
|
|
module ReenqueuerSleeper
|
|
|
|
# The block will run, and then sleep until the minimum duration. Returns the
|
|
|
|
# block's return value.
|
|
|
|
#
|
|
|
|
# Usage:
|
|
|
|
#
|
|
|
|
# ensure_minimum_duration(5.seconds) do
|
|
|
|
# # do something
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
def ensure_minimum_duration(minimum_duration)
|
2020-06-24 02:09:01 -04:00
|
|
|
start_time = Time.current
|
2020-01-03 04:07:33 -05:00
|
|
|
|
|
|
|
result = yield
|
|
|
|
|
|
|
|
sleep_if_time_left(minimum_duration, start_time)
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def sleep_if_time_left(minimum_duration, start_time)
|
|
|
|
time_left = calculate_time_left(minimum_duration, start_time)
|
|
|
|
|
|
|
|
sleep(time_left) if time_left > 0
|
|
|
|
end
|
|
|
|
|
|
|
|
def calculate_time_left(minimum_duration, start_time)
|
|
|
|
minimum_duration - elapsed_time(start_time)
|
|
|
|
end
|
|
|
|
|
|
|
|
def elapsed_time(start_time)
|
2020-06-24 02:09:01 -04:00
|
|
|
Time.current - start_time
|
2020-01-03 04:07:33 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|