ruby
43 lines · 7 steps
Debouncing search reindex jobs in Rails
A cache-backed lock collapses rapid record changes into a single delayed reindex job.
Explained by
highlit
1class SearchIndexReconciler
2 DEBOUNCE_WINDOW = 5.seconds
3
4 def self.schedule(record)
5 new(record).schedule
6 end
7
8 def initialize(record)
9 @record = record
10 @key = "reconcile:#{record.class.name.underscore}:#{record.id}"
11 end
12
13 def schedule
14 return if debounced?
15
16 ReconcileSearchIndexJob
17 .set(wait: DEBOUNCE_WINDOW)
18 .perform_later(@record.class.name, @record.id)
19
20 mark_scheduled
21 end
22
23 private
24
25 def debounced?
26 !Rails.cache.write(@key, true, unless_exist: true, expires_in: DEBOUNCE_WINDOW)
27 end
28
29 def mark_scheduled
30 Rails.logger.info("[#{self.class}] enqueued reconciliation for #{@key}")
31 end
32end
33
34class ReconcileSearchIndexJob < ApplicationJob
35 queue_as :indexing
36 discard_on ActiveRecord::RecordNotFound
37
38 def perform(class_name, id)
39 record = class_name.constantize.find(id)
40 Rails.cache.delete("reconcile:#{class_name.underscore}:#{id}")
41 Search::Indexer.new(record).reindex!
42 end
43end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An atomic cache write with unless_exist gives you a distributed debounce lock for free.
- 2Deferring the job with a wait window lets bursts of changes settle into one reindex.
- 3Deleting the debounce key inside the job reopens the window so future edits get scheduled.
Related explainers
ruby
class ParamCoercer TYPES = { integer: ->(v) { Integer(v) }, float: ->(v) { Float(v) },
Coercing request params by schema in Ruby
lambdas
type-coercion
lookup-table
Intermediate
7 steps
typescript
type AsyncFn<A extends unknown[], R> = (...args: A) => Promise<R>; interface MemoizeOptions<A extends unknown[]> { keyFn?: (...args: A) => string;
Memoizing async functions with TTL in TypeScript
memoization
generics
promises
Advanced
8 steps
ruby
module Api module V2 class ArticlesController < Api::BaseController before_action :set_article, only: %i[show update destroy]
Building a versioned JSON API controller in Rails
rest-api
serialization
pagination
Intermediate
10 steps
ruby
class ArticlesController < ApplicationController before_action :set_article, only: %i[show edit update destroy publish] def edit
Policy-based authorization in a Rails controller
authorization
policy-object
before-action
Intermediate
9 steps
ruby
class ArticlesController < ApplicationController DEFAULT_LIMIT = 25 MAX_LIMIT = 100
Cursor-based pagination in a Rails controller
pagination
cursor
query-building
Intermediate
7 steps
javascript
const cache = new Map(); const inflight = new Map(); async function fetchResults(query) {
Building a typeahead with an LRU cache
caching
lru-eviction
request-deduplication
Intermediate
9 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/debouncing-search-reindex-jobs-in-rails-explained-ruby-5e22/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.