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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Cache writes with `unless_exist: true` act as an atomic lock to guard against duplicate work.
- 2Hashing normalized arguments gives you a stable, collision-resistant identity for each unique job.
- 3Always release the lock in an `ensure` block so a failed job doesn't stay blocked forever.
Related explainers
java
@Component public class RegionCacheWarmer implements SmartInitializingSingleton { private static final Logger log = LoggerFactory.getLogger(RegionCacheWarmer.class);
Warming a Spring cache at startup
caching
startup-hook
dependency-injection
Intermediate
7 steps
go
package breaker import ( "errors"
How a circuit breaker works in Go
circuit-breaker
state-machine
concurrency
Intermediate
8 steps
ruby
class WebhookSignatureConstraint def initialize(provider) @provider = provider end
Verifying webhook signatures with Rails routing constraints
routing constraints
hmac
webhooks
Advanced
7 steps
java
public class TimedFetchService { private final ExecutorService executor = Executors.newFixedThreadPool(8); private final HttpClient httpClient = HttpClient.newHttpClient();
Enforcing HTTP timeouts with a Future
concurrency
timeouts
thread-pool
Intermediate
8 steps
ruby
class OrderMailerPreview < ActionMailer::Preview def confirmation OrderMailer.confirmation(sample_order) end
Previewing Rails mailers with sample data
mailer-previews
test-fixtures
in-memory-objects
Beginner
6 steps
ruby
def render_table(rows) return "(no data)" if rows.empty? columns = rows.flat_map(&:keys).uniq
Rendering an ASCII table in Ruby
text-formatting
column-alignment
recursion
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/deduplicating-active-job-enqueues-in-rails-explained-ruby-a3b1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.