Class: Hyperion::Master
- Inherits:
-
Object
- Object
- Hyperion::Master
- 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:
-
‘: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.
-
‘: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
-
#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
constructor
A new instance of Master.
- #run ⇒ Object
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 68 69 70 71 |
# 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 @worker_max_rss_mb = @config.worker_max_rss_mb @worker_check_interval = @config.worker_check_interval || 30 @last_health_check = 0 # monotonic seconds @cycling = {} # pid => true while we wait for it to exit end |
Class Method Details
.detect_worker_model ⇒ Object
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
#run ⇒ Object
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 98 99 100 101 |
# File 'lib/hyperion/master.rb', line 73 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 |