Class: PumaPlus::Shepherd

Inherits:
Object
  • Object
show all
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

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_kvObject



90
91
92
# File 'lib/puma_plus/shepherd.rb', line 90

def heartbeat_kv
  [["workers", active_count], ["worker_pids", @children.keys.join(",")]]
end

#on_idleObject

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

#quiesceObject



80
81
82
83
# File 'lib/puma_plus/shepherd.rb', line 80

def quiesce
  @respawn_on_death = false
  log "quiescing: will not respawn"
end

#runObject



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

Returns:

  • (Boolean)


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