Class: Hyperion::CLI
- Inherits:
-
Object
- Object
- Hyperion::CLI
- Defined in:
- lib/hyperion/cli.rb
Constant Summary collapse
- DEFAULT_CONFIG_PATH =
'config/hyperion.rb'
Class Method Summary collapse
-
.parse_argv!(argv) ⇒ Object
Extracted from #run so the flag-to-cli_opts mapping can be unit-tested without booting a server.
- .run(argv) ⇒ Object
- .run_cluster(config, app, workers) ⇒ Object
- .run_single(config, app) ⇒ Object
Class Method Details
.parse_argv!(argv) ⇒ Object
Extracted from #run so the flag-to-cli_opts mapping can be unit-tested without booting a server. Returns [cli_opts, config_path]. Mutates argv in place (consumes flags, leaves the rackup path for the caller).
62 63 64 65 66 67 68 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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/hyperion/cli.rb', line 62 def self.parse_argv!(argv) cli_opts = {} config_path = nil parser = OptionParser.new do |o| o. = 'Usage: hyperion [options] config.ru' o.on('-C', '--config PATH', "Hyperion config file (default ./#{DEFAULT_CONFIG_PATH} if it exists)") do |p| config_path = p end o.on('-b', '--bind HOST', 'host (default 127.0.0.1)') { |h| cli_opts[:host] = h } o.on('-p', '--port PORT', Integer, 'port (default 9292)') { |p| cli_opts[:port] = p } o.on('-w', '--workers N', Integer, 'worker processes (0 = nprocessors)') { |w| cli_opts[:workers] = w } o.on('-t', '--threads N', Integer, 'Rack handler thread pool size (0 disables)') do |t| cli_opts[:thread_count] = t end o.on('--tls-cert PATH', 'TLS certificate (PEM; chained intermediates supported)') do |p| # Parse every BEGIN/END block in the file — production certs ship # as leaf+intermediate(s) bundled together. `OpenSSL::X509::Certificate.new` # only reads the first block, so loading via that single call would # silently drop the chain. See Hyperion::TLS.parse_pem_chain. certs = Hyperion::TLS.parse_pem_chain(File.read(p)) abort("[hyperion] no certificates found in #{p}") if certs.empty? cli_opts[:tls_cert] = certs.first cli_opts[:tls_chain] = certs[1..] end o.on('--tls-key PATH', 'TLS private key (PEM)') do |p| cli_opts[:tls_key] = OpenSSL::PKey.read(File.read(p)) end o.on('--log-level LEVEL', %w[debug info warn error fatal], 'log level (default info)') do |l| cli_opts[:log_level] = l.to_sym end o.on('--log-format FORMAT', %w[text json auto], 'log format: text | json | auto (default auto: json on RAILS_ENV/RACK_ENV=production, colored text on TTY, json otherwise)') do |f| cli_opts[:log_format] = f.to_sym end o.on('--[no-]log-requests', 'Per-request access log line (default ON; pass --no-log-requests to disable).') do |v| cli_opts[:log_requests] = v end o.on('--fiber-local-shim', 'Patch Thread.current[] to be fiber-local (Rails-compat for older gems)') do cli_opts[:fiber_local_shim] = true end o.on('--[no-]yjit', 'Enable Ruby YJIT (default: auto on RAILS_ENV/RACK_ENV=production/staging)') do |v| cli_opts[:yjit] = v end o.on('--[no-]async-io', 'Run plain HTTP/1.1 connections under Async::Scheduler (required for hyperion-async-pg and other fiber-cooperative I/O; default off)') do |v| cli_opts[:async_io] = v end o.on('--max-body-bytes BYTES', Integer, 'Maximum request body size in bytes (default 16777216 = 16 MiB)') do |n| cli_opts[:max_body_bytes] = n end o.on('--max-header-bytes BYTES', Integer, 'Maximum total request-header size in bytes (default 65536 = 64 KiB)') do |n| cli_opts[:max_header_bytes] = n end o.on('--max-pending COUNT', Integer, 'Maximum queued connections per worker before new accepts are rejected with 503 (default unbounded)') do |n| cli_opts[:max_pending] = n end o.on('--max-request-read-seconds SECONDS', Float, 'Total wallclock budget for reading request line + headers + body (default 60.0; 0 disables)') do |n| cli_opts[:max_request_read_seconds] = n end # Security-sensitive: read the token verbatim and never echo it back # in any subsequent log/help line. argv is visible via `ps` on most # systems; production deployments should prefer --admin-token-file. o.on('--admin-token TOKEN', "Bearer token for the /-/quit and /-/metrics admin endpoints. \ WARNING: argv is visible via `ps`; prefer --admin-token-file PATH for production.") do |t| cli_opts[:admin_token] = t end o.on('--admin-token-file PATH', 'Read the admin token from a file. File must NOT be world-readable (perms must mask 0o007).') do |p| cli_opts[:admin_token] = read_admin_token_file(p) end o.on('--worker-max-rss-mb MB', Integer, 'Recycle a worker when its RSS exceeds MB megabytes (default unset; nil disables)') do |n| cli_opts[:worker_max_rss_mb] = n end o.on('--idle-keepalive SECONDS', Float, 'Idle keep-alive timeout in seconds (default 5.0)') do |n| cli_opts[:idle_keepalive] = n end o.on('--graceful-timeout SECONDS', Integer, 'Graceful shutdown deadline in seconds before SIGKILL (default 30)') do |n| cli_opts[:graceful_timeout] = n end o.on('-h', '--help', 'show help') do puts o exit 0 end end parser.parse!(argv) [cli_opts, config_path] end |
.run(argv) ⇒ Object
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/hyperion/cli.rb', line 13 def self.run(argv) cli_opts, config_path = parse_argv!(argv) # Precedence: CLI > config file > built-in default. We auto-load # config/hyperion.rb if present so operators can drop a file in their # repo and have it take effect without having to remember -C. config_path ||= DEFAULT_CONFIG_PATH if File.exist?(DEFAULT_CONFIG_PATH) config = config_path ? Hyperion::Config.load(config_path) : Hyperion::Config.new config.merge_cli!(cli_opts) # Install logger early so every subsequent log call honours the operator's # chosen format/level (config file or CLI) before anything else logs. if config.log_level || config.log_format Hyperion.logger = Hyperion::Logger.new(level: config.log_level, format: config.log_format) end # Propagate log_requests so every Connection picks it up via # `Hyperion.log_requests?` without needing to thread it through # Server/ThreadPool/Master plumbing. Default is ON; nil means "don't # touch — fall through to the env/default chain in Hyperion.log_requests?". Hyperion.log_requests = config.log_requests unless config.log_requests.nil? # Enable YJIT before workers fork / connections start. Auto-on in # production/staging gives operators the perf bump for free; explicit # config.yjit (true/false) overrides the env-based default. maybe_enable_yjit(config) rackup = argv.first || 'config.ru' abort("[hyperion] no such rackup file: #{rackup}") unless File.exist?(rackup) if config.fiber_local_shim Hyperion::FiberLocal.install! Hyperion.logger.info { { message: 'FiberLocal shim installed' } } end app = load_rack_app(rackup) app = wrap_admin_middleware(app, config) workers = config.workers.zero? ? Etc.nprocessors : config.workers if workers <= 1 run_single(config, app) else run_cluster(config, app, workers) end end |
.run_cluster(config, app, workers) ⇒ Object
214 215 216 217 218 219 |
# File 'lib/hyperion/cli.rb', line 214 def self.run_cluster(config, app, workers) tls = build_tls_from_config(config) Master.new(host: config.host, port: config.port, app: app, workers: workers, tls: tls, thread_count: config.thread_count, read_timeout: config.read_timeout, config: config).run end |
.run_single(config, app) ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/hyperion/cli.rb', line 163 def self.run_single(config, app) tls = build_tls_from_config(config) server = Server.new(host: config.host, port: config.port, app: app, tls: tls, thread_count: config.thread_count, read_timeout: config.read_timeout, max_pending: config.max_pending, max_request_read_seconds: config.max_request_read_seconds, h2_settings: Master.build_h2_settings(config), async_io: config.async_io) server.listen scheme = tls ? 'https' : 'http' Hyperion.logger.info { { message: 'listening', url: "#{scheme}://#{server.host}:#{server.port}" } } warn_c_parser_unavailable # Pre-allocate Rack env-pool entries and eager-touch lazy constants. # In single-mode there's no fork, but the warmup still pays for itself # by frontloading the first-N-request allocation cost off the first # real client. Idempotent — safe to call once per process. Hyperion.warmup! # Single-worker mode reuses the lifecycle hooks: before_fork is a no-op # here (no fork happens), and on_worker_boot/on_worker_shutdown fire # for the lone in-process "worker" so app code that opens DB pools etc. # gets the same lifecycle whether you run 1 or N workers. config.on_worker_boot.each { |h| h.call(0) } shutdown_r, shutdown_w = IO.pipe %w[INT TERM].each do |sig| Signal.trap(sig) do shutdown_w.write_nonblock('!') rescue StandardError nil end end shutdown_thread = Thread.new do shutdown_r.read(1) server.stop end shutdown_thread.report_on_exception = false server.start shutdown_thread.join config.on_worker_shutdown.each { |h| h.call(0) } # Drain per-thread access buffers + sync stdio. Single-worker mode # doesn't go through Master#shutdown_children, so without this call # buffered access lines + final shutdown messages can be lost on # SIGTERM. See Hyperion::Logger#flush_all. Hyperion.logger.flush_all end |