test: cover JobsQueueTest and teardown
This commit is contained in:
1 parent
c0f208e83a
commit
c26e4c77dc
1 file changed
+187
@@ -0,0 +1,187 @@
|
||||
require 'minitest/autorun'
|
||||
require 'prism'
|
||||
require 'tmpdir'
|
||||
require 'fileutils'
|
||||
|
||||
class JobsQueueTest < Minitest::Test
|
||||
def setup
|
||||
@dir = Dir.mktmpdir('prism-jobs-queue')
|
||||
@queue = Prism::Jobs::Queue.new(File.join(@dir, 'jobs.pstore'))
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.remove_entry(@dir)
|
||||
end
|
||||
|
||||
def test_enqueue_returns_an_incrementing_id
|
||||
first = @queue.enqueue(job_class: 'X', args: [])
|
||||
second = @queue.enqueue(job_class: 'X', args: [])
|
||||
assert_equal first + 1, second
|
||||
end
|
||||
|
||||
def test_next_ready_returns_the_earliest_due_pending_job
|
||||
later = @queue.enqueue(job_class: 'X', args: [1], run_at: Time.now - 5)
|
||||
sooner = @queue.enqueue(job_class: 'X', args: [2], run_at: Time.now - 50)
|
||||
job = @queue.next_ready
|
||||
assert_equal sooner, job[:id]
|
||||
refute_equal later, job[:id]
|
||||
end
|
||||
|
||||
def test_jobs_scheduled_in_the_future_are_not_ready
|
||||
@queue.enqueue(job_class: 'X', args: [], run_at: Time.now + 3600)
|
||||
assert_nil @queue.next_ready
|
||||
end
|
||||
|
||||
def test_next_ready_respects_queue_name_filter
|
||||
@queue.enqueue(job_class: 'X', args: [], queue_name: 'mailers')
|
||||
critical_id = @queue.enqueue(job_class: 'X', args: [], queue_name: 'critical')
|
||||
job = @queue.next_ready(queue_name: 'critical')
|
||||
assert_equal critical_id, job[:id]
|
||||
end
|
||||
|
||||
def test_claim_next_returns_the_earliest_due_pending_job_and_marks_it_processing
|
||||
later = @queue.enqueue(job_class: 'X', args: [1], run_at: Time.now - 5)
|
||||
sooner = @queue.enqueue(job_class: 'X', args: [2], run_at: Time.now - 50)
|
||||
|
||||
job = @queue.claim_next!
|
||||
assert_equal sooner, job[:id]
|
||||
refute_equal later, job[:id]
|
||||
assert_equal 'processing', job[:status]
|
||||
assert_equal 'processing', @queue.pending_jobs.find { |j| j[:id] == sooner }[:status]
|
||||
end
|
||||
|
||||
def test_claim_next_never_returns_the_same_job_twice
|
||||
id = @queue.enqueue(job_class: 'X', args: [])
|
||||
first = @queue.claim_next!
|
||||
second = @queue.claim_next!
|
||||
assert_equal id, first[:id]
|
||||
assert_nil second
|
||||
end
|
||||
|
||||
def test_claim_next_respects_queue_name_filter
|
||||
@queue.enqueue(job_class: 'X', args: [], queue_name: 'mailers')
|
||||
critical_id = @queue.enqueue(job_class: 'X', args: [], queue_name: 'critical')
|
||||
job = @queue.claim_next!(queue_name: 'critical')
|
||||
assert_equal critical_id, job[:id]
|
||||
end
|
||||
|
||||
def test_claim_next_is_nil_when_nothing_is_ready
|
||||
@queue.enqueue(job_class: 'X', args: [], run_at: Time.now + 3600)
|
||||
assert_nil @queue.claim_next!
|
||||
end
|
||||
|
||||
# Real cross-process race: two OS processes sharing the same PStore file
|
||||
# both try to grab the same batch of jobs at once. #claim_next! selects
|
||||
# and flips status to 'processing' inside one flock'd write transaction,
|
||||
# so the file itself is the arbiter of who wins each job - no job should
|
||||
# ever be claimed by more than one process.
|
||||
def test_claim_next_is_safe_across_real_concurrent_processes
|
||||
queue_path = File.join(@dir, 'jobs.pstore')
|
||||
job_count = 40
|
||||
job_count.times { @queue.enqueue(job_class: 'X', args: []) }
|
||||
|
||||
claims_dir = File.join(@dir, 'claims')
|
||||
FileUtils.mkdir_p(claims_dir)
|
||||
claimer_script = File.join(@dir, 'claimer.rb')
|
||||
File.write(claimer_script, <<~RUBY)
|
||||
require 'prism'
|
||||
queue = Prism::Jobs::Queue.new(#{queue_path.inspect})
|
||||
claims_dir = #{claims_dir.inspect}
|
||||
loop do
|
||||
job = queue.claim_next!
|
||||
break unless job
|
||||
File.write(File.join(claims_dir, "\#{job[:id]}.claim"), Process.pid.to_s)
|
||||
end
|
||||
RUBY
|
||||
|
||||
pids = 4.times.map do
|
||||
Process.spawn(RbConfig.ruby, '-I', File.expand_path('../lib', __dir__), claimer_script,
|
||||
out: File::NULL, err: File::NULL)
|
||||
end
|
||||
pids.each { |pid| Process.waitpid(pid) }
|
||||
|
||||
claim_files = Dir.glob(File.join(claims_dir, '*.claim'))
|
||||
assert_equal job_count, claim_files.size, 'every enqueued job should have been claimed exactly once'
|
||||
assert_equal claim_files.size, claim_files.map { |f| File.basename(f) }.uniq.size,
|
||||
'no job id should have produced more than one claim file'
|
||||
end
|
||||
|
||||
def test_mark_completed_removes_the_job
|
||||
id = @queue.enqueue(job_class: 'X', args: [])
|
||||
@queue.mark_completed(id)
|
||||
assert_nil @queue.next_ready
|
||||
assert_equal 0, @queue.size
|
||||
end
|
||||
|
||||
def test_mark_failed_reschedules_with_exponential_backoff_until_max_attempts
|
||||
id = @queue.enqueue(job_class: 'X', args: [], max_attempts: 3, backoff: { strategy: :exponential, base_delay: 2 })
|
||||
before = Time.now
|
||||
@queue.mark_failed(id, 'boom')
|
||||
job = @queue.pending_jobs.first
|
||||
assert_equal 1, job[:attempts]
|
||||
assert_equal 'pending', job[:status]
|
||||
assert_equal 'boom', job[:last_error]
|
||||
assert_in_delta (before + 2).to_f, job[:run_at].to_f, 1.0
|
||||
|
||||
@queue.mark_failed(id, 'boom again')
|
||||
job = @queue.pending_jobs.first
|
||||
assert_equal 2, job[:attempts]
|
||||
assert_in_delta (before + 4).to_f, job[:run_at].to_f, 1.0
|
||||
end
|
||||
|
||||
def test_mark_failed_dead_letters_after_exhausting_max_attempts
|
||||
id = @queue.enqueue(job_class: 'X', args: [], max_attempts: 2)
|
||||
@queue.mark_failed(id, 'first failure')
|
||||
@queue.mark_failed(id, 'second failure')
|
||||
|
||||
assert_equal 0, @queue.size
|
||||
assert_equal 1, @queue.dead_letter_size
|
||||
dead = @queue.dead_jobs.first
|
||||
assert_equal 'dead', dead[:status]
|
||||
assert_equal 'second failure', dead[:last_error]
|
||||
end
|
||||
|
||||
def test_retry_dead_moves_a_dead_job_back_to_pending
|
||||
id = @queue.enqueue(job_class: 'X', args: [], max_attempts: 1)
|
||||
@queue.mark_failed(id, 'fatal')
|
||||
assert_equal 1, @queue.dead_letter_size
|
||||
|
||||
@queue.retry_dead(id)
|
||||
assert_equal 0, @queue.dead_letter_size
|
||||
job = @queue.next_ready
|
||||
assert_equal id, job[:id]
|
||||
assert_equal 0, job[:attempts]
|
||||
assert_nil job[:last_error]
|
||||
end
|
||||
|
||||
def test_linear_backoff_grows_by_a_constant_step
|
||||
id = @queue.enqueue(job_class: 'X', args: [], max_attempts: 5, backoff: { strategy: :linear, base_delay: 3 })
|
||||
before = Time.now
|
||||
@queue.mark_failed(id, 'e1')
|
||||
job = @queue.pending_jobs.first
|
||||
assert_in_delta (before + 3).to_f, job[:run_at].to_f, 1.0
|
||||
|
||||
@queue.mark_failed(id, 'e2')
|
||||
job = @queue.pending_jobs.first
|
||||
assert_in_delta (before + 6).to_f, job[:run_at].to_f, 1.0
|
||||
end
|
||||
|
||||
def test_state_survives_reopening_the_store
|
||||
id = @queue.enqueue(job_class: 'PersistedJob', args: [42])
|
||||
path = File.join(@dir, 'jobs.pstore')
|
||||
reopened = Prism::Jobs::Queue.new(path)
|
||||
job = reopened.next_ready
|
||||
assert_equal id, job[:id]
|
||||
assert_equal [42], job[:args]
|
||||
end
|
||||
|
||||
def test_clear_empties_both_tables
|
||||
@queue.enqueue(job_class: 'X', args: [])
|
||||
dead_id = @queue.enqueue(job_class: 'X', args: [], max_attempts: 1)
|
||||
@queue.mark_failed(dead_id, 'boom')
|
||||
|
||||
@queue.clear!
|
||||
assert_equal 0, @queue.size
|
||||
assert_equal 0, @queue.dead_letter_size
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user