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

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 boot_embedded
  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?

  embed_in_web?(app) ? :embed : :refuse
end

.boot_embeddedObject

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 boot_embedded
  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.message}" }
  nil
end

.boot_swarmObject



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.message}" }
  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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


99
100
101
102
103
# File 'lib/wurk/rails_boot.rb', line 99

def embed_in_web?(app = ::Rails.application)
  app.config.wurk&.embed_in_web == 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

.loggerObject



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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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_countObject

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&.options&.[](:workers)
  return workers.to_i if workers

  ENV['WEB_CONCURRENCY']&.to_i
rescue StandardError
  nil
end

.refuse_preforking_bootObject



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.

Returns:

  • (Boolean)


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