Class: PumaPlus::Shepherd
- Inherits:
-
Object
- Object
- PumaPlus::Shepherd
- Defined in:
- lib/puma_plus/shepherd.rb
Overview
The shepherd process: loads the Rack app once, forks worker processes, and holds the control connection to the Go server.
Why a shepherd rather than letting Go fork Ruby directly: fork must happen from a process that has already loaded the application, so children inherit a warm heap through copy-on-write. Go cannot do that. This mirrors puma's cluster master (puma/lib/puma/cluster.rb) but is far smaller, because Go owns the listener, the queue, and every scaling decision -- the shepherd only executes commands.
Constant Summary collapse
- RESPAWN_BACKOFF =
1.0
Instance Method Summary collapse
- #heartbeat_kv ⇒ Object
-
#initialize(socket_path:, app_path:, workers:, threads:, config_path: nil, logger: $stderr) ⇒ Shepherd
constructor
A new instance of Shepherd.
-
#on_idle ⇒ Object
Reaping cannot wait on the socket: a child can die at any moment, and the CHLD handler only sets a flag because a trap context cannot safely take locks or allocate and Process.wait can block.
- #quiesce ⇒ Object
- #run ⇒ Object
-
#running? ⇒ Boolean
--- ControlChannel handler protocol ---.
-
#set_slots(target) ⇒ Object
SET_SLOTS carries the desired worker process count.
- #shutdown(grace_ms) ⇒ Object
Constructor Details
#initialize(socket_path:, app_path:, workers:, threads:, config_path: nil, logger: $stderr) ⇒ Shepherd
Returns a new instance of Shepherd.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/puma_plus/shepherd.rb', line 22 def initialize(socket_path:, app_path:, workers:, threads:, config_path: nil, logger: $stderr) @socket_path = socket_path @app_path = app_path @config_path = config_path @initial_workers = workers @threads = threads @logger = logger @children = {} # pid => worker_id # Pids we killed on purpose. Without this, reap_children cannot tell a # crash from a retirement and respawns the worker it was just told to # retire -- which produced a kill/respawn loop that burned 25 forks in a # scenario needing 3, and failed every request in flight on each dying # worker. @retiring = {} @next_worker_id = 0 @running = true @completed = 0 end |
Instance Method Details
#heartbeat_kv ⇒ Object
90 91 92 |
# File 'lib/puma_plus/shepherd.rb', line 90 def heartbeat_kv [["workers", active_count], ["worker_pids", @children.keys.join(",")]] end |
#on_idle ⇒ Object
Reaping cannot wait on the socket: a child can die at any moment, and the CHLD handler only sets a flag because a trap context cannot safely take locks or allocate and Process.wait can block.
97 98 99 100 101 102 |
# File 'lib/puma_plus/shepherd.rb', line 97 def on_idle return unless @child_died @child_died = false reap_children end |
#quiesce ⇒ Object
80 81 82 83 |
# File 'lib/puma_plus/shepherd.rb', line 80 def quiesce @respawn_on_death = false log "quiescing: will not respawn" end |
#run ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/puma_plus/shepherd.rb', line 42 def run # Preload before the first fork. This is the whole point of the shepherd: # every child inherits an already-parsed application, so forks are cheap # and copy-on-write keeps them cheap in memory too. @app = Worker.new( socket_path: @socket_path, app_path: @app_path, threads: @threads, logger: @logger ).send(:load_app) log "app preloaded from #{@app_path}" @hooks = Hooks.load(@config_path, logger: @logger) log "hooks from #{@hooks.path}: #{@hooks.names.join(', ')}" if @hooks.names.any? # Once, before ANY worker is forked -- puma's semantics # (puma/lib/puma/cluster.rb:438). The documented use is closing connections # opened during preload, so that each child dials its own rather than # inheriting a socket that several processes then share. @hooks.run(:before_fork) @control = ControlChannel.new(socket_path: @socket_path, logger: @logger).connect! install_signal_handlers @initial_workers.times { spawn_worker } @respawn_on_death = true @control.run(self) ensure shutdown_children @control&.close end |
#running? ⇒ Boolean
--- ControlChannel handler protocol ---
75 |
# File 'lib/puma_plus/shepherd.rb', line 75 def running? = @running |
#set_slots(target) ⇒ Object
SET_SLOTS carries the desired worker process count.
78 |
# File 'lib/puma_plus/shepherd.rb', line 78 def set_slots(target) = adjust_workers(target) |
#shutdown(grace_ms) ⇒ Object
85 86 87 88 |
# File 'lib/puma_plus/shepherd.rb', line 85 def shutdown(grace_ms) log "shutdown requested, grace #{grace_ms}ms" @running = false end |