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
-
.build_h2_settings(config) ⇒ Object
Pulls the four configurable HTTP/2 SETTINGS values out of the Config and returns them as a Hash.
- .detect_worker_model ⇒ Object
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.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/hyperion/master.rb', line 64 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
.build_h2_settings(config) ⇒ Object
Pulls the four configurable HTTP/2 SETTINGS values out of the Config and returns them as a Hash. Nils are stripped so an operator who explicitly sets one to ‘nil` (meaning “leave protocol-http2 default in place”) doesn’t accidentally send a SETTINGS entry with a nil value. Empty hash → no override → Http2Handler skips the SETTINGS push.
55 56 57 58 59 60 61 62 |
# File 'lib/hyperion/master.rb', line 55 def self.build_h2_settings(config) { max_concurrent_streams: config.h2_max_concurrent_streams, initial_window_size: config.h2_initial_window_size, max_frame_size: config.h2_max_frame_size, max_header_list_size: config.h2_max_header_list_size }.compact end |
.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
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/hyperion/master.rb', line 87 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 # Pre-allocate Rack env-pool entries and eager-touch lazy constants # BEFORE we fork. Children inherit the warm memory via copy-on-write # so the first batch of requests on each fresh worker doesn't pay # the allocation/autoload tax. Hyperion.warmup! # `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 |