Class: Hyperion::Master

Inherits:
Object
  • Object
show all
Defined in:
lib/hyperion/master.rb

Overview

Pre-fork master process. Owns the supervision loop. Each worker is a full fiber-scheduler ‘Hyperion::Server` running its own accept loop.

rc15 — per-OS worker model. There are two ways to give N children a listening socket on the same port:

  1. ‘:reuseport` (Linux): each worker binds its OWN socket with SO_REUSEPORT. The kernel hashes incoming connections across the sibling sockets — no thundering herd, no shared accept lock, linear scaling with worker count. The master never binds.

  2. ‘:share` (macOS / BSD / everything else): the master binds a single TCPServer (or SSLServer) BEFORE fork. Children inherit the fd via fork(2) and race on `accept(2)` — whichever child wins gets the connection. This is Puma’s model. We use it on Darwin because Darwin’s SO_REUSEPORT distributor hashes unevenly: at ‘-w 4` against a real Rails app a single curl probe cannot get answered inside 120s in the worst case, because the kernel keeps routing to a worker whose accept queue is already full.

Detection: ‘RbConfig::CONFIG` matching `linux` picks `:reuseport`; everything else picks `:share`. Operators can pin the mode explicitly with `HYPERION_WORKER_MODEL=share|reuseport` (used by the test suite to exercise both paths on a single host).

Constant Summary collapse

DEFAULT_WORKER_COUNT =

nil → Etc.nprocessors

nil
GRACEFUL_TIMEOUT_SECONDS =
30
WORKER_MODELS =
%i[reuseport share].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host:, port:, app:, workers: DEFAULT_WORKER_COUNT, read_timeout: Server::DEFAULT_READ_TIMEOUT_SECONDS, tls: nil, thread_count: Server::DEFAULT_THREAD_COUNT, config: nil) ⇒ Master

Returns a new instance of Master.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/hyperion/master.rb', line 50

def initialize(host:, port:, app:, workers: DEFAULT_WORKER_COUNT,
               read_timeout: Server::DEFAULT_READ_TIMEOUT_SECONDS, tls: nil,
               thread_count: Server::DEFAULT_THREAD_COUNT, config: nil)
  @host         = host
  @port         = port
  @app          = app
  @workers      = workers || Etc.nprocessors
  @read_timeout = read_timeout
  @tls          = tls
  @thread_count = thread_count
  @config       = config || Hyperion::Config.new
  @graceful_timeout = @config.graceful_timeout || GRACEFUL_TIMEOUT_SECONDS
  @children     = {} # pid => worker_index
  @next_index   = 0
  @stopping     = false
  @worker_model = self.class.detect_worker_model
  @listener     = nil # populated only in :share mode
end

Class Method Details

.detect_worker_modelObject



39
40
41
42
43
44
45
46
47
48
# File 'lib/hyperion/master.rb', line 39

def self.detect_worker_model
  override = ENV['HYPERION_WORKER_MODEL']&.to_sym
  return override if WORKER_MODELS.include?(override)

  host_os = RbConfig::CONFIG['host_os'].to_s
  case host_os
  when /linux/ then :reuseport
  else :share # macOS, BSD, anything else: shared-FD model (Puma-style)
  end
end

Instance Method Details

#runObject



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/hyperion/master.rb', line 69

def run
  install_signal_handlers
  bind_master_listener if @worker_model == :share
  Hyperion.logger.info do
    {
      message: 'master starting',
      pid: Process.pid,
      workers: @workers,
      host: @host,
      port: @port,
      worker_model: @worker_model
    }
  end

  # `before_fork` runs ONCE in the master before any worker is forked.
  # Operators use it to close shared resources (DB pools, Redis sockets)
  # so each child gets fresh connections rather than inheriting the
  # parent's open fds. Mirrors Puma's hook of the same name.
  @config.before_fork.each(&:call)

  @workers.times { spawn_worker }

  supervise
ensure
  # The master keeps the listener open across its lifetime so it can
  # respawn workers (the new fork inherits the same fd). It only gets
  # closed here once the master itself is exiting.
  @listener&.close
end