Last active
December 26, 2025 16:55
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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