Skip to content

Instantly share code, notes, and snippets.

@sobrinho
Last active December 26, 2025 16:55
Show Gist options
  • Select an option

  • Save sobrinho/85c3a2dd4865c1fe63b7c8d59b41bf14 to your computer and use it in GitHub Desktop.

Select an option

Save sobrinho/85c3a2dd4865c1fe63b7c8d59b41bf14 to your computer and use it in GitHub Desktop.
Use this to reload a Rails 3.2 application with Sidekiq 4.2 as it happens on Puma
# frozen_string_literal: true
require "thread"
# A writer-preferred read/write lock for Ruby.
#
# Semantics:
# - Multiple threads may hold the read lock concurrently.
# - Only one thread may hold the write lock at a time.
# - While a writer is active, no readers or other writers may proceed.
# - Writer-preferred: once any writer arrives (even if it must wait),
# new readers are blocked until all queued writers have run.
# This prevents writer starvation under heavy read load.
#
# Counters (explicit):
# - @readers_waiting: threads currently waiting to acquire the read lock.
# - @reading_active: threads currently inside the read section.
# - @writers_waiting: threads currently waiting to acquire the write lock.
# - @writing_active: threads currently inside the write section (0 or 1).
#
# Mechanics:
# - @mutex protects all counters and invariants.
# - @cv_read is used to park readers while any writer is active OR waiting.
# - @cv_write is used to park writers until there are no active readers/writers.
#
# IMPORTANT: Always use `while` around ConditionVariable waits.
# Threads may wake spuriously, and multiple threads may be awakened at once.
class MRSWLock
def initialize
@mutex = Mutex.new
@cv_read = ConditionVariable.new
@cv_write = ConditionVariable.new
@readers_waiting = 0
@reading_active = 0
@writers_waiting = 0
@writing_active = 0
end
def with_read_lock
@mutex.synchronize do
@readers_waiting += 1
# Writer-preferred: block readers if any writer is active OR waiting.
@cv_read.wait(@mutex) while @writing_active > 0 || @writers_waiting > 0
@readers_waiting -= 1
@reading_active += 1
end
begin
yield
ensure
@mutex.synchronize do
@reading_active -= 1
# Last ACTIVE reader out wakes exactly one writer (if any are queued).
@cv_write.signal if @reading_active == 0 && @writers_waiting > 0
end
end
end
def with_write_lock
@mutex.synchronize do
@writers_waiting += 1
# Wait until there are no active readers and no active writer.
@cv_write.wait(@mutex) while @reading_active > 0 || @writing_active > 0
# Become the active writer.
@writers_waiting -= 1
@writing_active += 1
end
begin
yield
ensure
@mutex.synchronize do
@writing_active -= 1
if @writers_waiting > 0
# More writers queued: wake exactly one writer.
@cv_write.signal
else
# No writers queued: allow all readers to race in.
@cv_read.broadcast
end
end
end
end
end
# Loader to reload code in development.
#
# Mirrors Rails' reloading logic (Rails 3.2 finisher):
# - If cache_classes is false (reloading enabled):
# - If reload_classes_only_on_change is true: use reloaders (reload only if changed)
# - If reload_classes_only_on_change is false: reload unconditionally
#
# Concurrency model:
# - Each job runs under a read lock (many can run concurrently).
# - Reload runs under a write lock (exclusive).
# - Writer-preferred: once a reload is requested, new jobs will block until the reload finishes.
#
# This guarantees no job executes while constants are being reloaded.
class SidekiqReloader
def initialize
@mrsw_lock = RWLock.new
end
def call
return yield if Rails.application.config.cache_classes
@mrsw_lock.with_write_lock do
if Rails.application.config.reload_classes_only_on_change
Rails.application.reloaders.each(&:execute_if_updated)
else
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_active_connections!
end
end
@mrsw_lock.with_read_lock do
yield
end
end
end
Sidekiq.configure_server do |config|
config.options[:reloader] = SidekiqReloader.new if Rails.env.development?
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment