Skip to content

Instantly share code, notes, and snippets.

@kjohnston
Last active March 6, 2023 15:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kjohnston/77868d3777bf48c4c2ed to your computer and use it in GitHub Desktop.
Save kjohnston/77868d3777bf48c4c2ed to your computer and use it in GitHub Desktop.
Base job class for delayed_job & exception_notification
class BaseJob
# External interface for calling the service
def self.call(object_id, opts={})
if perform_asynchronously?
Delayed::Job.enqueue(EnqueuedJob.new(name, object_id, opts))
else
perform(object_id, opts)
end
end
# Internal interface for calling the service
def self.perform(object_id, opts={})
new(object_id, opts).call
end
# Run jobs asynchronously only when load would be a factor
def self.perform_asynchronously?
Rails.env.in?(%w(production staging))
end
# Class that holds error detail for exception_notification
JobError = Struct.new(:message, :backtrace)
# Class that holds failure detail for exception_notification
JobFailure = Struct.new(:message, :backtrace)
# Class that holds success detail for exception_notification
JobSuccess = Struct.new(:message, :backtrace)
# Class that serializes well for Delayed::Job
EnqueuedJob = Struct.new(:klass, :object_id, :opts) do
# The method that Delayed::Job calls, which then invokes the actual job class
def perform
klass.constantize.perform(object_id, opts)
end
# Delayed::Job hook fired upon each run of the job if it results in a failure
def error(job, original_exception)
return unless job.attempts.zero? # Only notify upon first failure
message = "Job ##{job.id} Error (First Run)"
exception = JobError.new(message, [original_exception.backtrace])
deliver_notification(exception, exception_data(job))
end
# Delayed::Job hook fired upon final run of the job if it results in a failure
def failure(job)
message = "Job ##{job.id} Failure (Final)"
exception = JobFailure.new(message, [job.last_error])
deliver_notification(exception, exception_data(job))
end
# Delayed::Job hook fired upon success of the job
def success(job)
return unless job.attempts > 0 # Only notify on actual retry success
message = "Job ##{job.id} Success (Upon Retry)"
exception = JobSuccess.new(message, [""])
deliver_notification(exception, exception_data(job))
end
private
def deliver_notification(exception, data={})
ExceptionNotifier::Notifier
.background_exception_notification(exception, data: data).deliver
end
def exception_data(job)
{
job_id: job.id,
klass: klass,
object_id: object_id,
opts: opts
}
end
end
end
# Example job class relying on the standard signature and exception
# handling provided by the base job class.
#
# Invoke from wherever you need to like so:
# `PostFile::Import.call(1000, upload_id: 1234)`
#
# In development & test, the job will run synchronously.
# In staging & production, the job will run asynchronously.
# (Override self.perform_asynchronously to adjust).
#
class PostFile::Import < BaseJob
attr_reader :post, :upload
def initialize(post_id, opts={})
opts = HashWithIndifferentAccess.new(opts)
@post = Post.find(post_id)
@upload = post.file_uploads.pending.find(opts[:upload_id])
end
def call
return if upload.blank?
if post.files.create(file_params)
upload.mark_imported!
end
end
private
def file_params
{
upload_id: upload.id,
user_id: upload.user_id,
remote_file_url: upload.private_url
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment