Module: Wurk::RailsBoot
- Defined in:
- lib/wurk/rails_boot.rb
Overview
Coordinates how Wurk boots inside a Rails host: whether this process should boot at all, whether it may fork the swarm, and the boot itself. The Railtie owns only the Rails hooks (config namespace + server_mode initializer + after_initialize) and delegates every decision and side effect here, so the boot policy stays pure and unit-testable without the Railtie DSL. See docs/idea/03-process-model.md for the exact ordering.
Class Method Summary collapse
-
.boot(app = ::Rails.application) ⇒ Object
Invoked from after_initialize, once the host app has fully initialized.
-
.boot_action(app = ::Rails.application) ⇒ Object
What boot should do once skip_boot? is false.
-
.boot_embedded ⇒ Object
Sidekiq-embedded parity: a threads-only worker inside the web process, no fork.
- .boot_swarm ⇒ Object
-
.building? ⇒ Boolean
A build/precompile step must never fork the swarm (#247).
-
.embed_in_web?(app = ::Rails.application) ⇒ Boolean
Host opt-in (config/application.rb): run workers as in-process threads instead of forking, like Sidekiq embedded.
-
.enter_server_mode_if_serving(app = ::Rails.application) ⇒ Object
Invoked from the
wurk.server_modeinitializer, before config/initializers load. - .logger ⇒ Object
-
.preforking_web_server? ⇒ Boolean
Preforking / clustered web servers (Puma cluster, Unicorn, Passenger) fork their own worker processes.
-
.puma_cluster? ⇒ Boolean
Puma only preforks in cluster mode (workers > 0); single mode is threaded and safe to co-host the swarm.
-
.puma_worker_count ⇒ Object
Worker count from Puma's parsed CLI config when the server booted it, else from WEB_CONCURRENCY (the near-universal convention for Puma workers).
- .refuse_preforking_boot ⇒ Object
-
.skip_boot? ⇒ Boolean
A process that won't run workers isn't a server: skip both server mode and the swarm boot.
Class Method Details
.boot(app = ::Rails.application) ⇒ Object
Invoked from after_initialize, once the host app has fully initialized.
27 28 29 30 31 32 33 34 35 |
# File 'lib/wurk/rails_boot.rb', line 27 def boot(app = ::Rails.application) return if skip_boot? case boot_action(app) when :fork then boot_swarm when :embed then when :refuse then refuse_preforking_boot end end |
.boot_action(app = ::Rails.application) ⇒ Object
What boot should do once skip_boot? is false. Pure — reads env / loaded constants / host config, no side effects — so the boot decision is unit-testable without forking:
:fork — safe to fork the swarm in the background (the default).
:refuse — a preforking web server owns process forking here; don't.
:embed — host opted into in-process threads-only via embed_in_web.
53 54 55 56 57 |
# File 'lib/wurk/rails_boot.rb', line 53 def boot_action(app = ::Rails.application) return :fork unless preforking_web_server? (app) ? :embed : :refuse end |
.boot_embedded ⇒ Object
Sidekiq-embedded parity: a threads-only worker inside the web process, no fork. Redis validation failure keeps the host serving HTTP (log + carry on). at_exit drains on the graceful shutdown the web server runs on TERM.
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/wurk/rails_boot.rb', line 128 def instance = Wurk::Embedded.new(Wurk.configuration) instance.run at_exit { instance.stop } logger.info { 'wurk: running embedded in the web process (config.wurk.embed_in_web) — threads only, no fork' } instance rescue StandardError => e logger.error { "wurk: embedded boot failed: #{e.class}: #{e.}" } nil end |
.boot_swarm ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/wurk/rails_boot.rb', line 105 def boot_swarm swarm = Wurk::Swarm.new(topology: Wurk.configuration.topology, shutdown_timeout: Wurk.configuration[:timeout] || Swarm::DEFAULT_SHUTDOWN_TIMEOUT) # Co-hosted in the web process (e.g. Puma single mode): the host owns the # process-wide TERM/INT traps. Installing the swarm's own would hijack # them — a deploy TERM would drain the swarm but never stop the HTTP # server. Let the host keep signal ownership and drain the swarm on its # graceful exit (same contract as boot_embedded). swarm.boot(install_signals: false) at_exit { swarm.shutdown } # supervise must still run somewhere or crashed children never respawn and # memory checks never fire. A background thread keeps the host's main # thread free to serve HTTP. Thread.new do swarm.supervise rescue StandardError => e logger.error { "wurk supervisor thread died: #{e.class}: #{e.}" } end end |
.building? ⇒ Boolean
A build/precompile step must never fork the swarm (#247). The default
Rails Dockerfile runs SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile; that loads :environment → fires after_initialize,
but there's no Redis during docker build, so a fork would hang/fail the
build. Same for other env-loading rake tasks (db:prepare, db:migrate).
The real server path is unaffected: rails server / puma boot through
Rails::Command, not Rake, and don't set the dummy secret.
168 169 170 171 172 173 174 |
# File 'lib/wurk/rails_boot.rb', line 168 def building? return true if ENV.key?('SECRET_KEY_BASE_DUMMY') defined?(::Rake) && ::Rake.application.top_level_tasks.any? rescue StandardError false end |
.embed_in_web?(app = ::Rails.application) ⇒ Boolean
Host opt-in (config/application.rb): run workers as in-process threads instead of forking, like Sidekiq embedded. Set it in application.rb, not an initializer — server mode is decided before initializers load.
99 100 101 102 103 |
# File 'lib/wurk/rails_boot.rb', line 99 def (app = ::Rails.application) app.config.wurk&. == true rescue StandardError false end |
.enter_server_mode_if_serving(app = ::Rails.application) ⇒ Object
Invoked from the wurk.server_mode initializer, before config/initializers
load. This Rails process forks the workers, so it IS the server. Enter
server mode now — otherwise the app's Sidekiq.configure_server blocks
gate on config.server? (still false) and are silently dropped. A process
that won't run workers (skip_boot?, or one that refuses to boot under a
preforking web server) is not a server.
19 20 21 22 23 24 |
# File 'lib/wurk/rails_boot.rb', line 19 def enter_server_mode_if_serving(app = ::Rails.application) return if skip_boot? return if boot_action(app) == :refuse Wurk.enter_server_mode end |
.logger ⇒ Object
157 158 159 |
# File 'lib/wurk/rails_boot.rb', line 157 def logger Wurk.configuration.logger end |
.preforking_web_server? ⇒ Boolean
Preforking / clustered web servers (Puma cluster, Unicorn, Passenger)
fork their own worker processes. Forking the swarm from after_initialize
in one of them is the highest-risk boot path: without app preloading every
server-worker re-runs the hook and forks its own full swarm (N×
oversubscription); with preloading the swarm supervisor ends up entangled
with the server's own fork/signal supervision. Detect the common three.
65 66 67 68 69 70 |
# File 'lib/wurk/rails_boot.rb', line 65 def preforking_web_server? return true if defined?(::PhusionPassenger) return true if defined?(::Unicorn) puma_cluster? end |
.puma_cluster? ⇒ Boolean
Puma only preforks in cluster mode (workers > 0); single mode is threaded and safe to co-host the swarm. Best-effort: a missed cluster falls through to the historical fork path; an over-eager match is escapable via embed_in_web / WURK_DISABLED / running the swarm as its own process.
76 77 78 79 80 81 |
# File 'lib/wurk/rails_boot.rb', line 76 def puma_cluster? return false unless defined?(::Puma) count = puma_worker_count count.is_a?(Integer) && count.positive? end |
.puma_worker_count ⇒ Object
Worker count from Puma's parsed CLI config when the server booted it, else from WEB_CONCURRENCY (the near-universal convention for Puma workers). Guarded — the accessor and options shape vary across Puma versions.
86 87 88 89 90 91 92 93 94 |
# File 'lib/wurk/rails_boot.rb', line 86 def puma_worker_count cfg = ::Puma.cli_config if ::Puma.respond_to?(:cli_config) workers = cfg&.&.[](:workers) return workers.to_i if workers ENV['WEB_CONCURRENCY']&.to_i rescue StandardError nil end |
.refuse_preforking_boot ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/wurk/rails_boot.rb', line 139 def refuse_preforking_boot logger.warn { <<~MSG } wurk: preforking web server detected (Puma cluster / Unicorn / Passenger). Refusing to fork the worker swarm from a process that forks its own workers. Run the swarm as its own process instead: bundle exec wurkswarm # forked swarm, real parallelism bundle exec wurk # single process, thread pool Or run workers inside this web process (threads only, no fork): # config/application.rb config.wurk.embed_in_web = true Already running workers elsewhere? Set WURK_DISABLED=1 here to silence this. MSG end |
.skip_boot? ⇒ Boolean
A process that won't run workers isn't a server: skip both server mode and the swarm boot. Console mode is detected reliably here — the console command file defines ::Rails::Console before initializers run.
40 41 42 43 44 45 |
# File 'lib/wurk/rails_boot.rb', line 40 def skip_boot? ENV['WURK_DISABLED'] == '1' || building? || defined?(::Rails::Console) || ::Rails.env.test? end |