I'm in the process of building an email reminder feature for students. They should receive an email at an interval that their instructor specifies indicating how much progress they have made.
My approach is to use ActiveJob to schedule a job that sends an email, then when the job is performed, I use the after_perform
callback to schedule the next job (taken from this post). Here's what that looks like:
class StudentReminderJob < ApplicationJob
queue_as :default
after_perform do |job|
# (get user update frequency preference)
self.class.set(wait: update_frequency).perform_later(job.arguments[0])
end
def perform(user)
# Send email containing student stats information
UserMailer.send_student_reminder_email(user)
end
end
The initial set of reminders will be kicked off by a rake task.
My issue is this: when trying to test the above code with perform_enqueued_jobs
I seem to be hitting an infinite loop. Here's my test:
require 'rails_helper'
describe StudentReminderJob, active_job: true, type: :job do
include ActiveJob::TestHelper
include ActiveSupport::Testing::TimeHelpers
let(:user) { create :user }
subject(:job) { described_class.perform_later(user) }
it 'gets enqueued' do
# Passes
expect { job }.to have_enqueued_job(described_class)
end
it 'receives perform later with user and base URL' do
# Passes (may be duplicatative)
expect(StudentReminderJob).to receive(:perform_later).with(user)
job
end
it 'calls UserMailer with #send_student_reminder_email when performed' do
# Fails - (SystemStackError: stack level too deep)
expect(UserMailer).to receive(:send_student_reminder_email)
perform_enqueued_jobs { job }
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end
The third it block fails with SystemStackError: stack level too deep
. Is there a way to only perform the one job in the queue and avoid performing the job from the callback?
Ideally, I'd then like to expand my test coverage to ensure that a new job is enqueued at a given date and time.
I suspect that the issue here is that the perform_enqueued_jobs
helper attempts to execute all jobs in the queue until that point, but the queue list is never exhausted, because each job queues another job (hence the loop).
Try this,
perform_later
.currently_queued_job = enqueued_jobs.first
.perform_enqueued_jobs
but use the :only
option to filter/limit the jobs that will get executed. For instance,perform_enqueued_jobs(
only: ->(job) { job['job_id'] == currently_queued_job['job_id'] }
)
Here's the Rails documentation for the perform_enqueued_jobs
, mentioning:
:only
and:except
options accepts Class, Array of Class or Proc. When passed a Proc, an instance of the job will be passed as argument.