Class: PumaPlus::Worker

Inherits:
Object
  • Object
show all
Defined in:
lib/puma_plus/worker.rb

Overview

A Ruby worker process: loads the Rack app once, then runs N threads that each dial the Go server and serve requests.

Phase 1 runs a single worker process directly. Phase 4 puts a shepherd above this that forks several of these and handles SET_SLOTS/QUIESCE.

Instance Method Summary collapse

Constructor Details

#initialize(socket_path:, app_path:, threads:, worker_id: 0, config_path: nil, hooks: nil, multiprocess: false, logger: $stderr) ⇒ Worker

Returns a new instance of Worker.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/puma_plus/worker.rb', line 16

def initialize(socket_path:, app_path:, threads:, worker_id: 0, config_path: nil,
               hooks: nil, multiprocess: false, logger: $stderr)
  @socket_path = socket_path
  @app_path = app_path
  @threads = threads
  @worker_id = worker_id
  @logger = logger
  # The shepherd passes hooks it already loaded, so a forked child does not
  # re-read and re-evaluate the config file once per worker.
  @hooks = hooks || Hooks.load(config_path, logger: logger)
  # Matches puma: multithread when more than one thread can call the app in
  # this process, multiprocess when a shepherd is managing worker processes
  # (puma/lib/puma/binder.rb:33-34).
  @multithread = threads > 1
  @multiprocess = multiprocess
end

Instance Method Details

#runObject



33
34
35
36
37
38
# File 'lib/puma_plus/worker.rb', line 33

def run
  # Load before any thread dials in. A connection appearing on the Go side is
  # the readiness signal, so it must not appear until the app can actually
  # serve -- that is the whole reason puma-plus needs no dial-polling.
  run_preloaded(load_app)
end

#run_preloaded(app) ⇒ Object

Serve using an already-loaded app. Used by the shepherd, which loads once before forking so children inherit a warm heap through copy-on-write.



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
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/puma_plus/worker.rb', line 42

def run_preloaded(app)
  # Start GVL instrumentation before any thread serves, so the very first
  # request is measured. A no-op without the native extension.
  PumaPlus::GVL.start!

  # One outbound WebSocket control channel per worker process, opened before
  # any thread serves. The app can then address any connection at any time,
  # from any thread, rather than only while handling a message.
  PumaPlus::WS.connect!(@socket_path, logger: @logger)

  # Before any thread dials in, which is what makes this ordering meaningful:
  # a connection appearing on the Go side is the readiness signal, so hooks
  # finishing first means Go cannot dispatch a request to a worker whose
  # setup has not completed.
  @hooks.run(:on_worker_boot, @worker_id)

  threads = @threads.times.map do |i|
    Thread.new do
      Thread.current.name = "puma-plus #{@worker_id}/#{i}"
      WorkerThread.new(
        socket_path: @socket_path,
        app: app,
        worker_id: @worker_id,
        thread_index: i,
        multithread: @multithread,
        multiprocess: @multiprocess,
        logger: @logger
      ).run
    end
  end

  @logger.puts "[puma-plus] worker #{@worker_id} pid=#{Process.pid} " \
               "serving with #{@threads} threads"

  trap("TERM") { threads.each(&:kill) }
  trap("INT") { threads.each(&:kill) }

  threads.each(&:join)
ensure
  # Not fatal: the process is leaving either way, and refusing to exit
  # because a cleanup hook raised helps nobody.
  @hooks.run(:on_worker_shutdown, @worker_id, fatal: false)
end