ruby 39 lines · 8 steps

Optimistic locking with retries in Rails

A Document model guards concurrent writes by matching a version number and retrying when another process wins the race.

Explained by highlit
1class Document < ApplicationRecord
2 class StaleObjectError < StandardError
3 def initialize(id)
4 super("Document ##{id} was modified by another process")
5 end
6 end
7 
8 def update_with_lock!(attributes, expected_version:)
9 transaction do
10 rows = self.class
11 .where(id: id, lock_version: expected_version)
12 .update_all(
13 attributes.merge(
14 lock_version: expected_version + 1,
15 updated_at: Time.current
16 )
17 )
18 
19 raise StaleObjectError, id if rows.zero?
20 
21 assign_attributes(attributes)
22 self.lock_version = expected_version + 1
23 self
24 end
25 end
26 
27 def apply_changes(changes, expected_version:, retries: 3)
28 attempt = 0
29 begin
30 reload if attempt.positive?
31 version = attempt.zero? ? expected_version : lock_version
32 update_with_lock!(changes, expected_version: version)
33 rescue StaleObjectError
34 attempt += 1
35 retry if attempt <= retries
36 raise
37 end
38 end
39end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Optimistic locking lets you detect concurrent writes by checking a version column instead of holding a database lock.
  2. 2Making the update conditional on the expected version and inspecting the affected-row count turns a lost race into a catchable error.
  3. 3Wrapping the write in a bounded retry loop that reloads fresh state lets most conflicts resolve automatically without failing the caller.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Optimistic locking with retries in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code