Skip to content

Instantly share code, notes, and snippets.

@masutaka
Created December 11, 2019 15:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masutaka/12943fb977b47b1a6c67a1cbdace4141 to your computer and use it in GitHub Desktop.
Save masutaka/12943fb977b47b1a6c67a1cbdace4141 to your computer and use it in GitHub Desktop.
sample one-shot job on Heroku
# @abstract One-shot 関連のジョブは必ずこの class を継承し、
# キーワード引数に :global_executions を要求する #perform を実装すること
class OneshotBaseJob < ApplicationJob
class << self
# Retry the One-shot job due to the exception
#
# @param job [OneshotBaseJob] A One-shot job class to retry
# @return [void]
#
# @note See ActiveJob::Exceptions::ClassMethods#retry_on for the parameters except to `job`
def retry_oneshot_on(exception, job: self, wait: 3.seconds, attempts: 5, queue: nil, priority: nil)
rescue_from exception do |error|
if executions < attempts
global_executions = executions + 1
determine_delay = forked_determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions)
logger.error "Retrying #{job} in #{determine_delay} seconds (#{global_executions}/#{attempts}), due to a #{exception}. The original exception was #{error.cause.inspect}."
job.set(
wait: determine_delay,
queue: queue,
priority: priority,
).perform_later(
**arguments.first,
global_executions: global_executions,
)
else
logger.error "Stopped retrying #{job} due to a #{exception}, which reoccurred on #{executions} attempts. The original exception was #{error.cause.inspect}."
raise error
end
end
end
end
# One-shot(一回限り)で起動する関係で、executions(実行回数)が繰り越されない。
# 永久にリトライすることがあるため、引数で global_executions を注入し、手動で繰り越す。
before_perform do |job|
Rails.logger.info(
'%s is performed (release_version: %s, commit_hash: %s)' % [
self.class,
Settings.heroku.release_version,
Settings.heroku.slug_commit,
],
)
shop_id = job.arguments.first[:shop_id]
job.executions = job.arguments.first[:global_executions]
end
# One-shot Job を実行する
#
# @param global_executions [Integer] 全体での実行回数。1 始まり
# @return [void]
# def perform(global_executions:, ...)
# ...
# end
private
# ActiveJob::Exceptions#determine_delay をそのまま持ってきた。
# https://github.com/rails/rails/blob/v6.0.0/activejob/lib/active_job/exceptions.rb#L124-L140
#
# やむを得ずそうした理由は以下のとおり。
# * Rails 5.2.3 から 6.0.0 で引数が変わった。private method なので変更に気づき続けるのが難しい
# * OneshotJob では Active Job 以上のことをし始めており、このメソッドを使う必要がある
def forked_determine_delay(seconds_or_duration_or_algorithm:, executions:)
case seconds_or_duration_or_algorithm
when :exponentially_longer
(executions**4) + 2
when ActiveSupport::Duration
duration = seconds_or_duration_or_algorithm
duration.to_i
when Integer
seconds = seconds_or_duration_or_algorithm
seconds
when Proc
algorithm = seconds_or_duration_or_algorithm
algorithm.call(executions)
else
raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
end
end
end
class SampleCoreJob < OneshotBaseJob
# queue_as は敢えて設定しない。Oneshot Job として Heroku の One-off Dyno 上で動作し、
# sidekiq の queue には溜まらないため。
retry_oneshot_on(Sample1Error, job: SampleJob, wait: :exponentially_longer)
retry_oneshot_on(Sample2Error, job: SampleJob, wait: 5.minutes)
# サンプルジョブ
#
# @param shop_id [String] ショップの ID
# @param global_executions [Integer] 全体での実行回数。1 始まり
# @return [void]
def perform(shop_id:, global_executions:)
# ...
end
end
class SampleJob < OneshotBaseJob
queue_as :default
# One-Off Dyno を作成し、SampleCoreJob を実行するリクエストを発行する
#
# @param shop_id [String] ショップの ID
# @param global_executions [Integer] 全体での実行回数。1 始まり
# @return [void]
def perform(shop_id:, global_executions:)
dyno = ::PlatformAPI::Dyno.new(
::PlatformAPI.connect(Settings.heroku.api_key),
)
dyno.create(
Settings.heroku.app_name,
attach: false,
command: %!rails r "SampleCoreJob.perform_now(shop_id: '#{shop_id}', global_executions: #{global_executions})"!,
force_no_tty: nil,
size: Settings.heroku.oneshot_dyno_size,
type: 'run',
time_to_live: 1.hour,
)
end
end
@masutaka
Copy link
Author

masutaka commented Dec 11, 2019

SampleJob.perform_later(shop_id: 12345, global_executions: 1) のように使う。

global_executions には必ず 1 を指定すること。リトライするとインクリメントされて、次に実行される SampleJob に引き継がれる。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment