ruby 42 lines · 9 steps

Deduplicating Active Job enqueues in Rails

A concern uses a cache lock keyed by job arguments to skip duplicate enqueues and release the lock after the job runs.

Explained by highlit
1module UniqueJob
2 extend ActiveSupport::Concern
3 
4 class_methods do
5 def enqueue_unique(*args, expires_in: 10.minutes, **kwargs)
6 lock_key = unique_lock_key(args, kwargs)
7 
8 unless Rails.cache.write(lock_key, true, unless_exist: true, expires_in: expires_in)
9 Rails.logger.info("[#{name}] skipping duplicate enqueue for #{lock_key}")
10 return false
11 end
12 
13 set(unique_lock_key: lock_key).perform_later(*args, **kwargs)
14 end
15 
16 def unique_lock_key(args, kwargs)
17 digest = Digest::SHA256.hexdigest([args, kwargs.sort].to_json)
18 "unique_job:#{name}:#{digest}"
19 end
20 end
21 
22 included do
23 around_perform do |job, block|
24 block.call
25 ensure
26 key = job.arguments.last.is_a?(Hash) && job.arguments.last[:unique_lock_key]
27 Rails.cache.delete(key) if key.present?
28 end
29 end
30end
31 
32class RefreshAccountStatsJob < ApplicationJob
33 include UniqueJob
34 
35 queue_as :default
36 
37 def perform(account_id, **)
38 account = Account.find(account_id)
39 account.recalculate_statistics!
40 account.touch(:stats_refreshed_at)
41 end
42end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Cache writes with `unless_exist: true` act as an atomic lock to guard against duplicate work.
  2. 2Hashing normalized arguments gives you a stable, collision-resistant identity for each unique job.
  3. 3Always release the lock in an `ensure` block so a failed job doesn't stay blocked forever.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Deduplicating Active Job enqueues in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code