raptor 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ #include <linux/bpf.h>
2
+ #include <bpf/bpf_helpers.h>
3
+
4
+ #define MAX_WORKERS 64
5
+
6
+ // Per-worker listening sockets, keyed by worker index.
7
+ struct {
8
+ __uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
9
+ __type(key, __u32);
10
+ __type(value, __u64);
11
+ __uint(max_entries, MAX_WORKERS);
12
+ } socks SEC(".maps");
13
+
14
+ // Worker count at slot 0, per-worker backlog at slots 1..N.
15
+ struct {
16
+ __uint(type, BPF_MAP_TYPE_ARRAY);
17
+ __type(key, __u32);
18
+ __type(value, __u32);
19
+ __uint(max_entries, MAX_WORKERS + 1);
20
+ } loads SEC(".maps");
21
+
22
+ // Routes each incoming connection to the worker with the lowest backlog,
23
+ // with a random tie-break when loads are equal.
24
+ SEC("sk_reuseport")
25
+ int select_least_loaded(struct sk_reuseport_md *ctx) {
26
+ __u32 count_key = 0;
27
+ __u32 *count_ptr = bpf_map_lookup_elem(&loads, &count_key);
28
+ if (!count_ptr || *count_ptr == 0) {
29
+ return SK_DROP;
30
+ }
31
+ __u32 num_workers = *count_ptr;
32
+ if (num_workers > MAX_WORKERS) {
33
+ num_workers = MAX_WORKERS;
34
+ }
35
+
36
+ __u32 min_load = ~0u;
37
+ __u32 max_load = 0;
38
+ __u32 min_idx = 0;
39
+
40
+ for (__u32 worker_idx = 0; worker_idx < MAX_WORKERS; worker_idx++) {
41
+ if (worker_idx >= num_workers) {
42
+ break;
43
+ }
44
+ __u32 worker_key = worker_idx + 1;
45
+ __u32 *load_ptr = bpf_map_lookup_elem(&loads, &worker_key);
46
+ if (!load_ptr) {
47
+ continue;
48
+ }
49
+ if (*load_ptr < min_load) {
50
+ min_load = *load_ptr;
51
+ min_idx = worker_idx;
52
+ }
53
+ if (*load_ptr > max_load) {
54
+ max_load = *load_ptr;
55
+ }
56
+ }
57
+
58
+ __u32 chosen_idx = (min_load == max_load) ? (bpf_get_prandom_u32() % num_workers) : min_idx;
59
+ bpf_sk_select_reuseport(ctx, &socks, &chosen_idx, 0);
60
+ return SK_PASS;
61
+ }
62
+
63
+ char _license[] SEC("license") = "GPL";
@@ -2,6 +2,6 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
- append_cflags("-fvisibility=hidden")
5
+ append_cflags(["-fvisibility=hidden", "-Wno-type-limits"])
6
6
 
7
7
  create_makefile("raptor/raptor_http")
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ append_cflags("-fvisibility=hidden")
6
+
7
+ create_makefile("raptor/raptor_native")
@@ -0,0 +1,99 @@
1
+ #ifdef __linux__
2
+ #define _GNU_SOURCE 1
3
+ #endif
4
+
5
+ #include "ruby.h"
6
+ #include "ruby/io.h"
7
+ #include <sys/uio.h>
8
+ #include <errno.h>
9
+ #include <limits.h>
10
+
11
+ #ifdef __linux__
12
+ #include <sched.h>
13
+ #include <unistd.h>
14
+ #endif
15
+
16
+ static VALUE eEAGAINWaitWritable;
17
+
18
+ static VALUE raptor_native_writev_nonblock(VALUE self, VALUE io, VALUE strings) {
19
+ (void)self;
20
+ Check_Type(strings, T_ARRAY);
21
+ long len = RARRAY_LEN(strings);
22
+ if (len == 0) return LONG2NUM(0);
23
+ if (len > IOV_MAX) len = IOV_MAX;
24
+
25
+ int fd = rb_io_descriptor(io);
26
+ struct iovec *iov = alloca(len * sizeof(struct iovec));
27
+
28
+ for (long i = 0; i < len; i++) {
29
+ VALUE str = RARRAY_AREF(strings, i);
30
+ Check_Type(str, T_STRING);
31
+ iov[i].iov_base = RSTRING_PTR(str);
32
+ iov[i].iov_len = RSTRING_LEN(str);
33
+ }
34
+
35
+ ssize_t written;
36
+ while ((written = writev(fd, iov, (int)len)) < 0) {
37
+ if (errno == EINTR) continue;
38
+ if (errno == EAGAIN || errno == EWOULDBLOCK) rb_raise(eEAGAINWaitWritable, "writev would block");
39
+ rb_sys_fail("writev");
40
+ }
41
+
42
+ return LONG2NUM((long)written);
43
+ }
44
+
45
+ static VALUE raptor_native_pin_to_cpu(VALUE self, VALUE cpu) {
46
+ (void)self;
47
+ #ifdef __linux__
48
+ int worker_index = NUM2INT(cpu);
49
+ cpu_set_t current;
50
+ CPU_ZERO(&current);
51
+ if (sched_getaffinity(0, sizeof(current), &current) < 0) rb_sys_fail("sched_getaffinity");
52
+
53
+ int cpu_id = -1;
54
+ int seen = 0;
55
+ for (int candidate = 0; candidate < CPU_SETSIZE; candidate++) {
56
+ if (!CPU_ISSET(candidate, &current)) continue;
57
+ if (seen == worker_index) { cpu_id = candidate; break; }
58
+ seen++;
59
+ }
60
+ if (cpu_id < 0) return Qfalse;
61
+
62
+ cpu_set_t set;
63
+ CPU_ZERO(&set);
64
+ CPU_SET(cpu_id, &set);
65
+ if (sched_setaffinity(0, sizeof(set), &set) < 0) rb_sys_fail("sched_setaffinity");
66
+ return Qtrue;
67
+ #else
68
+ (void)cpu;
69
+ return Qfalse;
70
+ #endif
71
+ }
72
+
73
+ static VALUE raptor_native_cpu_count(VALUE self) {
74
+ (void)self;
75
+ #ifdef __linux__
76
+ cpu_set_t set;
77
+ CPU_ZERO(&set);
78
+ if (sched_getaffinity(0, sizeof(set), &set) < 0) rb_sys_fail("sched_getaffinity");
79
+ return LONG2NUM((long)CPU_COUNT(&set));
80
+ #else
81
+ return LONG2NUM(0);
82
+ #endif
83
+ }
84
+
85
+ RUBY_FUNC_EXPORTED void Init_raptor_native(void) {
86
+ rb_ext_ractor_safe(true);
87
+
88
+ VALUE mRaptor = rb_define_module("Raptor");
89
+ VALUE mVectorIO = rb_define_module_under(mRaptor, "VectorIO");
90
+ VALUE mCPU = rb_define_module_under(mRaptor, "CPU");
91
+
92
+ rb_define_singleton_method(mVectorIO, "writev_nonblock", raptor_native_writev_nonblock, 2);
93
+
94
+ rb_define_singleton_method(mCPU, "pin", raptor_native_pin_to_cpu, 1);
95
+ rb_define_singleton_method(mCPU, "count", raptor_native_cpu_count, 0);
96
+
97
+ eEAGAINWaitWritable = rb_const_get(rb_cIO, rb_intern("EAGAINWaitWritable"));
98
+ rb_global_variable(&eEAGAINWaitWritable);
99
+ }
@@ -78,18 +78,28 @@ module Rackup
78
78
  else
79
79
  config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
80
80
  end,
81
+ socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
82
+ drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
81
83
  workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
82
84
  ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
83
85
  threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
84
86
  app: app
85
87
  }
86
88
  result[:rackup] = config[:rackup] if config.key?(:rackup)
87
- result[:client] = cli_defaults[:client].merge(config[:client] || {})
88
- result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
89
+ result[:chdir] = config[:chdir] if config.key?(:chdir)
90
+ result[:environment] = config[:environment] if config.key?(:environment)
91
+ ::Raptor::CLI::NESTED_OPTION_KEYS.each do |key|
92
+ result[key] = cli_defaults[key].merge(config[key] || {})
93
+ end
89
94
  result[:worker_boot_timeout] = (config[:worker_boot_timeout] || cli_defaults[:worker_boot_timeout]).to_i
95
+ result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
96
+ result[:worker_drain_timeout] = (config[:worker_drain_timeout] || cli_defaults[:worker_drain_timeout]).to_i
90
97
  result[:worker_shutdown_timeout] = (config[:worker_shutdown_timeout] || cli_defaults[:worker_shutdown_timeout]).to_i
91
98
  result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
92
99
  result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
100
+ result[:stdout_file] = config[:stdout_file] if config.key?(:stdout_file)
101
+ result[:stderr_file] = config[:stderr_file] if config.key?(:stderr_file)
102
+ result[:access_log_file] = config[:access_log_file] if config.key?(:access_log_file)
93
103
  result[:on_error] = config[:on_error] if config.key?(:on_error)
94
104
  result
95
105
  end
data/lib/raptor/binder.rb CHANGED
@@ -56,7 +56,20 @@ module Raptor
56
56
  end
57
57
 
58
58
  # @rbs @bind_uris: Array[String]
59
+ # @rbs @socket_backlog: Integer
60
+ # @rbs @inherited_fds: Hash[String, Array[Integer]]
59
61
  # @rbs @listeners: Array[TCPServer | UNIXServer | SslListener]
62
+ # @rbs @uri_listeners: Hash[String, Array[TCPServer | UNIXServer | SslListener]]
63
+
64
+ # Array of bind URIs.
65
+ #
66
+ # @return [Array<String>] the bind URIs
67
+ attr_reader :bind_uris
68
+
69
+ # Kernel listen() queue depth for TCP/SSL listeners.
70
+ #
71
+ # @return [Integer] the socket backlog
72
+ attr_reader :socket_backlog
60
73
 
61
74
  # Array of listening sockets.
62
75
  #
@@ -67,19 +80,26 @@ module Raptor
67
80
  #
68
81
  # Parses the provided bind URIs and creates listening sockets for each one.
69
82
  # Supports tcp://, unix://, and ssl:// schemes. Localhost is expanded to
70
- # all available loopback addresses (both IPv4 and IPv6).
83
+ # all available loopback addresses (both IPv4 and IPv6). When `inherited_fds`
84
+ # supplies file descriptors for a URI, the listener is reconstructed from
85
+ # those FDs instead of binding fresh.
71
86
  #
72
87
  # @param bind_uris [Array<String>] array of URI strings to bind to
88
+ # @param socket_backlog [Integer] kernel listen() queue depth for TCP/SSL listeners
89
+ # @param inherited_fds [Hash{String => Array<Integer>}] inherited listener FDs keyed by bind URI
73
90
  # @return [void]
74
91
  # @raise [UnknownBindSchemeError] if a URI has an unsupported scheme
75
92
  #
76
93
  # @example
77
94
  # binder = Binder.new(["tcp://0.0.0.0:3000", "unix:///tmp/raptor.sock"])
78
95
  #
79
- # @rbs (Array[String] bind_uris) -> void
80
- def initialize(bind_uris)
96
+ # @rbs (Array[String] bind_uris, ?socket_backlog: Integer, ?inherited_fds: Hash[String, Array[Integer]]) -> void
97
+ def initialize(bind_uris, socket_backlog: SOCKET_BACKLOG, inherited_fds: {})
81
98
  @bind_uris = bind_uris
99
+ @socket_backlog = socket_backlog
100
+ @inherited_fds = inherited_fds
82
101
  @listeners = nil
102
+ @uri_listeners = nil
83
103
  parse
84
104
  end
85
105
 
@@ -133,28 +153,91 @@ module Raptor
133
153
  @listeners.each(&:close)
134
154
  end
135
155
 
156
+ # Returns the file descriptors of every listener, grouped by the bind URI
157
+ # they were created from. The result is the payload to hand to a successor
158
+ # process via the `inherited_fds:` constructor argument.
159
+ #
160
+ # @return [Hash{String => Array<Integer>}]
161
+ #
162
+ # @rbs () -> Hash[String, Array[Integer]]
163
+ def inheritable_fds
164
+ @uri_listeners.transform_values { |listeners| listeners.map { |listener| listener.to_io.fileno } }
165
+ end
166
+
167
+ # Clears the close-on-exec flag on every listener so the file descriptors
168
+ # survive `Kernel.exec`.
169
+ #
170
+ # @return [void]
171
+ #
172
+ # @rbs () -> void
173
+ def clear_close_on_exec
174
+ @listeners.each { |listener| listener.to_io.close_on_exec = false }
175
+ end
176
+
136
177
  private
137
178
 
138
- # Parses bind URIs and creates listening sockets.
179
+ # Parses bind URIs and creates listening sockets, reusing inherited file
180
+ # descriptors for URIs supplied in `@inherited_fds`.
139
181
  #
140
182
  # @return [void]
141
183
  # @raise [UnknownBindSchemeError] if a URI scheme is not supported
142
184
  #
143
185
  # @rbs () -> void
144
186
  def parse
145
- @listeners = @bind_uris.map do |bind_uri|
146
- uri = URI.parse(bind_uri)
147
- case uri.scheme
148
- when "tcp"
149
- create_tcp_listeners(uri.host, uri.port)
150
- when "unix"
151
- create_unix_listeners(uri.path)
152
- when "ssl"
153
- create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
187
+ @uri_listeners = @bind_uris.to_h do |bind_uri|
188
+ if filenos = @inherited_fds[bind_uri]
189
+ [bind_uri, restore_listeners(bind_uri, filenos)]
154
190
  else
155
- raise UnknownBindSchemeError.new(uri.scheme)
191
+ [bind_uri, create_listeners(bind_uri)]
156
192
  end
157
- end.tap(&:flatten!)
193
+ end
194
+ @listeners = @uri_listeners.values.flatten
195
+ end
196
+
197
+ # Creates fresh listeners for the given bind URI.
198
+ #
199
+ # @param bind_uri [String] the URI to bind
200
+ # @return [Array<TCPServer, UNIXServer, SslListener>]
201
+ # @raise [UnknownBindSchemeError] if the URI scheme is not supported
202
+ #
203
+ # @rbs (String bind_uri) -> Array[TCPServer | UNIXServer | SslListener]
204
+ def create_listeners(bind_uri)
205
+ uri = URI.parse(bind_uri)
206
+ case uri.scheme
207
+ when "tcp"
208
+ create_tcp_listeners(uri.host, uri.port)
209
+ when "unix"
210
+ create_unix_listeners(uri.path)
211
+ when "ssl"
212
+ create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
213
+ else
214
+ raise UnknownBindSchemeError.new(uri.scheme)
215
+ end
216
+ end
217
+
218
+ # Reconstructs listeners for the given bind URI from inherited file
219
+ # descriptors.
220
+ #
221
+ # @param bind_uri [String] the URI the FDs were bound to
222
+ # @param filenos [Array<Integer>] file descriptors to wrap
223
+ # @return [Array<TCPServer, UNIXServer, SslListener>]
224
+ # @raise [UnknownBindSchemeError] if the URI scheme is not supported
225
+ #
226
+ # @rbs (String bind_uri, Array[Integer] filenos) -> Array[TCPServer | UNIXServer | SslListener]
227
+ def restore_listeners(bind_uri, filenos)
228
+ uri = URI.parse(bind_uri)
229
+ case uri.scheme
230
+ when "tcp"
231
+ filenos.map { |fileno| TCPServer.for_fd(fileno) }
232
+ when "unix"
233
+ register_unix_socket_cleanup(uri.path)
234
+ filenos.map { |fileno| UNIXServer.for_fd(fileno) }
235
+ when "ssl"
236
+ ssl_context = build_ssl_context(URI.decode_www_form(uri.query || "").to_h)
237
+ filenos.map { |fileno| SslListener.new(tcp_server: TCPServer.for_fd(fileno), ssl_context: ssl_context) }
238
+ else
239
+ raise UnknownBindSchemeError.new(uri.scheme)
240
+ end
158
241
  end
159
242
 
160
243
  # Creates TCP server sockets for the given host and port.
@@ -171,11 +254,15 @@ module Raptor
171
254
 
172
255
  host = host[1..-2] if host&.start_with?("[")
173
256
 
174
- tcp_server = TCPServer.new(host, port)
175
- tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
176
- tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
177
- tcp_server.listen SOCKET_BACKLOG
257
+ addrinfo = Addrinfo.tcp(host, port)
258
+ socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
259
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
260
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
261
+ socket.bind(addrinfo)
262
+ socket.listen(@socket_backlog)
178
263
 
264
+ tcp_server = TCPServer.for_fd(socket.fileno)
265
+ socket.autoclose = false
179
266
  [tcp_server]
180
267
  end
181
268
 
@@ -200,11 +287,22 @@ module Raptor
200
287
  end
201
288
  end
202
289
 
203
- server = UNIXServer.new(path)
290
+ register_unix_socket_cleanup(path)
291
+
292
+ [UNIXServer.new(path)]
293
+ end
294
+
295
+ # Registers an `at_exit` hook that removes the Unix socket file on the
296
+ # owning master's clean exit. Each call records the current process so
297
+ # forked workers won't delete a socket their master still owns.
298
+ #
299
+ # @param path [String] filesystem path of the Unix socket
300
+ # @return [void]
301
+ #
302
+ # @rbs (String path) -> void
303
+ def register_unix_socket_cleanup(path)
204
304
  master_pid = Process.pid
205
305
  at_exit { File.delete(path) rescue nil if Process.pid == master_pid }
206
-
207
- [server]
208
306
  end
209
307
 
210
308
  # Creates SSL server sockets for the given host, port, and SSL parameters.
@@ -220,18 +318,28 @@ module Raptor
220
318
  #
221
319
  # @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
222
320
  def create_ssl_listeners(host, port, ssl_params)
223
- require "openssl"
224
-
225
321
  tcp_servers = create_tcp_listeners(host, port)
322
+ ssl_context = build_ssl_context(ssl_params)
323
+ tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
324
+ end
226
325
 
227
- ssl_context = OpenSSL::SSL::SSLContext.new
228
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
229
- ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
230
- ssl_context.alpn_protocols = ["h2", "http/1.1"]
231
- ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
232
- ssl_context.freeze
326
+ # Builds a frozen `OpenSSL::SSL::SSLContext` configured for HTTP/2 and
327
+ # HTTP/1.1 ALPN negotiation.
328
+ #
329
+ # @param ssl_params [Hash<String, String>] SSL options ("cert" and "key" paths)
330
+ # @return [OpenSSL::SSL::SSLContext]
331
+ #
332
+ # @rbs (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext
333
+ def build_ssl_context(ssl_params)
334
+ require "openssl"
233
335
 
234
- tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
336
+ OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
337
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
338
+ ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
339
+ ssl_context.alpn_protocols = ["h2", "http/1.1"]
340
+ ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
341
+ ssl_context.freeze
342
+ end
235
343
  end
236
344
 
237
345
  # Returns all available loopback IP addresses.
data/lib/raptor/cli.rb CHANGED
@@ -26,24 +26,41 @@ module Raptor
26
26
  class CLI
27
27
  DEFAULT_WORKER_COUNT = Etc.nprocessors
28
28
 
29
+ NESTED_OPTION_KEYS = [:connection, :http1, :http2].freeze
30
+
29
31
  DEFAULT_OPTIONS = {
30
32
  binds: ["tcp://0.0.0.0:9292"].freeze,
33
+ socket_backlog: 1024,
34
+ drain_accept_queue: false,
31
35
  workers: DEFAULT_WORKER_COUNT,
32
36
  ractors: 1,
33
37
  threads: 3,
34
38
  rackup: "config.ru",
35
- client: {
39
+ chdir: nil,
40
+ environment: nil,
41
+ connection: {
36
42
  first_data_timeout: 30,
37
43
  chunk_data_timeout: 10,
38
- persistent_data_timeout: 65,
44
+ write_timeout: 5,
39
45
  max_body_size: nil,
40
46
  body_spool_threshold: 1024 * 1024,
41
47
  },
42
- worker_timeout: 60,
48
+ http1: {
49
+ persistent_data_timeout: 65,
50
+ max_keepalive_requests: 100,
51
+ },
52
+ http2: {
53
+ max_concurrent_streams: 100,
54
+ },
43
55
  worker_boot_timeout: 60,
56
+ worker_timeout: 60,
57
+ worker_drain_timeout: 25,
44
58
  worker_shutdown_timeout: 30,
45
59
  stats_file: "tmp/raptor.json",
46
60
  pid_file: nil,
61
+ stdout_file: nil,
62
+ stderr_file: nil,
63
+ access_log_file: nil,
47
64
  }.freeze
48
65
 
49
66
  DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
@@ -102,14 +119,17 @@ module Raptor
102
119
  #
103
120
  # @rbs (Array[String] argv) -> void
104
121
  def initialize(argv)
122
+ @options = DEFAULT_OPTIONS.dup
123
+ NESTED_OPTION_KEYS.each { |key| @options[key] = @options[key].dup }
124
+ @options[:launch_command] = $PROGRAM_NAME
125
+ @options[:launch_argv] = argv.dup
126
+
105
127
  if argv.first == "stats"
106
128
  argv.shift
107
129
  @command = :stats
108
130
  else
109
131
  @command = :server
110
132
  end
111
- @options = DEFAULT_OPTIONS.dup
112
- @options[:client] = @options[:client].dup
113
133
 
114
134
  apply_config_file(extract_config_path(argv) || self.class.default_config_path)
115
135
 
@@ -178,9 +198,6 @@ module Raptor
178
198
 
179
199
  # Loads a config file and merges it into `@options` over the defaults.
180
200
  #
181
- # Top-level keys replace defaults; the nested `:client` hash is merged
182
- # key-by-key so a config file does not need to restate every client option.
183
- #
184
201
  # @param path [String, nil] path to the config file, or nil to no-op
185
202
  # @return [void]
186
203
  #
@@ -190,8 +207,8 @@ module Raptor
190
207
 
191
208
  config = self.class.load_config_file(path)
192
209
  config.each do |key, value|
193
- if key == :client && value.is_a?(Hash)
194
- @options[:client] = @options[:client].merge(value)
210
+ if NESTED_OPTION_KEYS.include?(key) && value.is_a?(Hash)
211
+ @options[key] = @options[key].merge(value)
195
212
  else
196
213
  @options[key] = value
197
214
  end
@@ -212,53 +229,85 @@ module Raptor
212
229
  end
213
230
 
214
231
  opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
215
- if @options[:binds] == DEFAULT_OPTIONS[:binds]
232
+ if @options[:binds].equal?(DEFAULT_OPTIONS[:binds])
216
233
  @options[:binds] = [bind]
217
234
  else
218
235
  @options[:binds] << bind
219
236
  end
220
237
  end
221
238
 
239
+ opts.on("--socket-backlog NUM", Integer, "Socket listen backlog (default: 1024)") do |num|
240
+ @options[:socket_backlog] = num
241
+ end
242
+
243
+ opts.on("--[no-]drain-accept-queue", "Drain the kernel accept queue on shutdown (default: off)") do |bool|
244
+ @options[:drain_accept_queue] = bool
245
+ end
246
+
222
247
  opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
223
248
  @options[:workers] = num
224
249
  end
225
250
 
226
- opts.on("-r", "--ractors NUM", Integer, "Number of ractors (default: 1)") do |num|
251
+ opts.on("-r", "--ractors NUM", Integer, "Number of pipeline ractors per worker (default: 1)") do |num|
227
252
  @options[:ractors] = num
228
253
  end
229
254
 
230
- opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
255
+ opts.on("-t", "--threads NUM", Integer, "Number of application threads per worker (default: 3)") do |num|
231
256
  @options[:threads] = num
232
257
  end
233
258
 
259
+ opts.on("-C", "--chdir PATH", String, "Change to PATH before loading the Rack application (default: none)") do |path|
260
+ @options[:chdir] = path
261
+ end
262
+
263
+ opts.on("-e", "--environment ENV", String, "Application environment label; falls back to $RAILS_ENV, then $RACK_ENV, then development") do |env|
264
+ @options[:environment] = env
265
+ end
266
+
234
267
  opts.on("--first-data-timeout SECONDS", Integer, "First data timeout in seconds (default: 30)") do |timeout|
235
- @options[:client][:first_data_timeout] = timeout
268
+ @options[:connection][:first_data_timeout] = timeout
236
269
  end
237
270
 
238
271
  opts.on("--chunk-data-timeout SECONDS", Integer, "Chunk data timeout in seconds (default: 10)") do |timeout|
239
- @options[:client][:chunk_data_timeout] = timeout
272
+ @options[:connection][:chunk_data_timeout] = timeout
240
273
  end
241
274
 
242
- opts.on("--persistent-data-timeout SECONDS", Integer, "Persistent data timeout in seconds (default: 65)") do |timeout|
243
- @options[:client][:persistent_data_timeout] = timeout
275
+ opts.on("--write-timeout SECONDS", Integer, "Per-write socket timeout in seconds (default: 5)") do |timeout|
276
+ @options[:connection][:write_timeout] = timeout
244
277
  end
245
278
 
246
279
  opts.on("--max-body-size BYTES", Integer, "Maximum request body size in bytes (default: unlimited)") do |bytes|
247
- @options[:client][:max_body_size] = bytes
280
+ @options[:connection][:max_body_size] = bytes
248
281
  end
249
282
 
250
- opts.on("--body-spool-threshold BYTES", Integer, "Spool request bodies larger than this to a tempfile (default: #{1024 * 1024})") do |bytes|
251
- @options[:client][:body_spool_threshold] = bytes
283
+ opts.on("--body-spool-threshold BYTES", Integer, "Request body spool threshold in bytes (default: #{1024 * 1024})") do |bytes|
284
+ @options[:connection][:body_spool_threshold] = bytes
252
285
  end
253
286
 
254
- opts.on("--worker-timeout SECONDS", Integer, "Worker check-in timeout in seconds (default: 60)") do |timeout|
255
- @options[:worker_timeout] = timeout
287
+ opts.on("--http1-persistent-data-timeout SECONDS", Integer, "HTTP/1.1 keep-alive idle timeout in seconds (default: 65)") do |timeout|
288
+ @options[:http1][:persistent_data_timeout] = timeout
289
+ end
290
+
291
+ opts.on("--http1-max-keepalive-requests NUM", Integer, "Maximum HTTP/1.1 requests per keep-alive connection (default: 100)") do |num|
292
+ @options[:http1][:max_keepalive_requests] = num
293
+ end
294
+
295
+ opts.on("--http2-max-concurrent-streams NUM", Integer, "Maximum HTTP/2 concurrent streams per connection (default: 100)") do |num|
296
+ @options[:http2][:max_concurrent_streams] = num
256
297
  end
257
298
 
258
299
  opts.on("--worker-boot-timeout SECONDS", Integer, "Worker boot timeout in seconds (default: 60)") do |timeout|
259
300
  @options[:worker_boot_timeout] = timeout
260
301
  end
261
302
 
303
+ opts.on("--worker-timeout SECONDS", Integer, "Worker check-in timeout in seconds (default: 60)") do |timeout|
304
+ @options[:worker_timeout] = timeout
305
+ end
306
+
307
+ opts.on("--worker-drain-timeout SECONDS", Integer, "Worker request-drain timeout in seconds (default: 25)") do |timeout|
308
+ @options[:worker_drain_timeout] = timeout
309
+ end
310
+
262
311
  opts.on("--worker-shutdown-timeout SECONDS", Integer, "Worker shutdown timeout in seconds (default: 30)") do |timeout|
263
312
  @options[:worker_shutdown_timeout] = timeout
264
313
  end
@@ -271,6 +320,18 @@ module Raptor
271
320
  @options[:pid_file] = path
272
321
  end
273
322
 
323
+ opts.on("--stdout-file PATH", String, "Redirect stdout to PATH; reopened on SIGHUP (default: none)") do |path|
324
+ @options[:stdout_file] = path
325
+ end
326
+
327
+ opts.on("--stderr-file PATH", String, "Redirect stderr to PATH; reopened on SIGHUP (default: none)") do |path|
328
+ @options[:stderr_file] = path
329
+ end
330
+
331
+ opts.on("--access-log-file PATH", String, "Write Common Log Format access logs to PATH; reopened on SIGHUP (default: none)") do |path|
332
+ @options[:access_log_file] = path
333
+ end
334
+
274
335
  opts.on("--help", "Show this help") do
275
336
  puts opts
276
337
  exit