ruby 41 lines · 9 steps

Batch-processing dormant users in Rails

A service object that archives, purges, and exports large user sets without loading everything into memory.

Explained by highlit
1class InactiveAccountCleanup
2 BATCH_SIZE = 500
3 INACTIVITY_THRESHOLD = 18.months
4 
5 def call
6 cutoff = INACTIVITY_THRESHOLD.ago
7 dormant = User.where("last_active_at < ?", cutoff).where(archived_at: nil)
8 
9 dormant.find_each(batch_size: BATCH_SIZE) do |user|
10 user.update_columns(archived_at: Time.current)
11 AccountArchivedMailer.with(user: user).farewell_notice.deliver_later
12 end
13 end
14 
15 def purge_soft_deleted!
16 scope = User.where.not(deleted_at: nil).where("deleted_at < ?", 30.days.ago)
17 
18 scope.in_batches(of: BATCH_SIZE) do |relation|
19 ids = relation.pluck(:id)
20 
21 Comment.where(user_id: ids).delete_all
22 ApiToken.where(user_id: ids).delete_all
23 relation.delete_all
24 
25 Rails.logger.info("Purged #{ids.size} users, oldest id #{ids.min}")
26 end
27 end
28 
29 def export_report(io)
30 User.select(:id, :email, :last_active_at)
31 .order(:id)
32 .find_in_batches(batch_size: 1_000) do |group|
33 rows = group.map do |user|
34 [user.id, user.email, user.last_active_at&.iso8601]
35 end
36 
37 io.write(rows.map { |row| row.join(",") }.join("\n"))
38 io.write("\n")
39 end
40 end
41end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Batched iteration keeps memory flat when acting on tables that don't fit comfortably in RAM.
  2. 2Choosing update_columns, delete_all, or a mailer per row is a deliberate trade of callbacks and speed.
  3. 3Deleting dependent rows by foreign key before the parent avoids orphaned records when callbacks are bypassed.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Batch-processing dormant users in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code