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
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 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.