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
Class Method Details
.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 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 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 |
# File 'lib/hyperion/cli.rb', line 13 def self.run(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('-h', '--help', 'show help') do puts o exit 0 end end parser.parse!(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
165 166 167 168 169 170 |
# File 'lib/hyperion/cli.rb', line 165 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
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 162 163 |
# File 'lib/hyperion/cli.rb', line 114 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 |