require "securerandom"
class RedisMutex
class LockError < StandardError; end
UNLOCK_SCRIPT = <<~LUA.freeze
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA
def initialize(redis, key, ttl_ms: 10_000)
@redis = redis
@key = key
@ttl_ms = ttl_ms
@token = SecureRandom.hex(20)
end
def acquire
@redis.set(@key, @token, nx: true, px: @ttl_ms) ? true : false
end
def release
@redis.eval(UNLOCK_SCRIPT, keys: [@key], argv: [@token]) == 1
end
def with_lock
return false unless acquire
begin
yield
true
ensure
release
end
end
end
class InvoiceFinalizerJob < ApplicationJob
queue_as :billing
RETRY_DELAY = 5.seconds
def perform(invoice_id)
invoice = Invoice.find(invoice_id)
return if invoice.finalized?
mutex = RedisMutex.new(RedisClient.current, lock_key(invoice_id), ttl_ms: 30_000)
got_lock = mutex.with_lock do
invoice.reload
next if invoice.finalized?
invoice.finalize!
PaymentGateway.capture(invoice)
InvoiceMailer.receipt(invoice).deliver_later
end
unless got_lock
self.class.set(wait: RETRY_DELAY).perform_later(invoice_id)
end
end
private
def lock_key(invoice_id)
"invoice:finalize:#{invoice_id}"
end
end
A distributed mutex coordinates work across many processes or machines that share nothing but a Redis instance. The pattern is simple in spirit — one key represents the lock, and only the holder may release it — but the details determine whether it is actually safe. This snippet shows a minimal, correct implementation and a realistic caller.
In RedisMutex, acquisition uses SET key token NX PX ttl. The NX flag makes the write succeed only when the key is absent, giving atomic mutual exclusion, and PX attaches an expiry in milliseconds. The TTL is the crucial safety valve: if the holder crashes or its network partitions away, the lock does not leak forever — it expires and another worker can proceed. Each acquisition generates a unique token (a random hex string) so the process can later prove ownership.
The subtle bug this design avoids is releasing a lock the caller no longer owns. Without a check, a slow worker whose lock already expired could DEL a key that a different worker has since acquired, silently breaking exclusion. release therefore runs the Lua script in UNLOCK_SCRIPT, which compares the stored value to the caller's token and deletes only on a match. Running compare-and-delete as a server-side Lua script makes it atomic, closing the check-then-act race. The with_lock helper wraps acquire/release in an ensure block and yields only when the lock is held, so cleanup always runs even if the block raises.
The caller, InvoiceFinalizerJob, guards a non-idempotent side effect — finalizing an invoice exactly once. It namespaces the key per record (invoice:finalize:<id>) so unrelated invoices never contend. When with_lock returns false, another worker already holds the lock, so the job re-enqueues itself with a short delay rather than blocking a thread.
A few trade-offs are worth noting. This is a single-Redis lock, not Redlock; it assumes a reasonably reliable primary and is unsuitable when correctness must survive Redis failover. The TTL must exceed the worst-case critical-section time, or the lock can expire mid-work — for long tasks a watchdog that periodically extends the TTL is the usual remedy. It is a practical, low-ceremony choice for deduplicating jobs and serializing access to external systems.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.