raptor 0.22.3 → 0.23.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Dockerfile +3 -1
  4. data/README.md +38 -31
  5. data/docs/raptor-vs-puma.md +44 -38
  6. data/docs/talk.md +1295 -0
  7. data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
  8. data/ext/raptor_http/extconf.rb +2 -0
  9. data/ext/raptor_http/raptor_http.c +91 -119
  10. data/ext/raptor_http2/raptor_http2.c +272 -223
  11. data/ext/raptor_native/raptor_native.c +38 -20
  12. data/lib/rackup/handler/raptor.rb +12 -11
  13. data/lib/raptor/binder.rb +12 -12
  14. data/lib/raptor/cli.rb +18 -18
  15. data/lib/raptor/cluster.rb +44 -44
  16. data/lib/raptor/control_server.rb +3 -2
  17. data/lib/raptor/detached_body.rb +6 -2
  18. data/lib/raptor/http.rb +7 -5
  19. data/lib/raptor/http1.rb +232 -207
  20. data/lib/raptor/http2.rb +312 -265
  21. data/lib/raptor/reactor.rb +138 -148
  22. data/lib/raptor/reuseport_bpf.rb +62 -38
  23. data/lib/raptor/server.rb +45 -34
  24. data/lib/raptor/stats.rb +15 -3
  25. data/lib/raptor/thread_locals.rb +4 -3
  26. data/lib/raptor/version.rb +1 -1
  27. data/sig/generated/rackup/handler/raptor.rbs +2 -1
  28. data/sig/generated/raptor/binder.rbs +9 -9
  29. data/sig/generated/raptor/cli.rbs +6 -6
  30. data/sig/generated/raptor/cluster.rbs +9 -9
  31. data/sig/generated/raptor/control_server.rbs +2 -1
  32. data/sig/generated/raptor/detached_body.rbs +5 -1
  33. data/sig/generated/raptor/http.rbs +3 -3
  34. data/sig/generated/raptor/http1.rbs +100 -91
  35. data/sig/generated/raptor/http2.rbs +200 -189
  36. data/sig/generated/raptor/reactor.rbs +107 -118
  37. data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
  38. data/sig/generated/raptor/server.rbs +26 -26
  39. data/sig/generated/raptor/stats.rbs +3 -2
  40. data/sig/generated/raptor/thread_locals.rbs +4 -3
  41. metadata +2 -2
  42. data/docs/brisrails-talk.md +0 -2398
@@ -4,27 +4,26 @@
4
4
 
5
5
  #include "ruby.h"
6
6
  #include "ruby/io.h"
7
- #include <sys/uio.h>
7
+ #include "ruby/thread.h"
8
8
  #include <errno.h>
9
9
  #include <limits.h>
10
+ #include <sys/uio.h>
10
11
 
11
12
  #ifdef __linux__
12
13
  #include <sched.h>
13
14
  #include <sys/prctl.h>
14
- #include <unistd.h>
15
15
  #endif
16
16
 
17
17
  static VALUE eEAGAINWaitWritable;
18
18
 
19
- static VALUE raptor_native_writev_nonblock(VALUE self, VALUE io, VALUE strings) {
20
- (void)self;
19
+ static VALUE vector_io_writev_nonblock(VALUE self, VALUE io, VALUE strings) {
21
20
  Check_Type(strings, T_ARRAY);
22
21
  long len = RARRAY_LEN(strings);
23
22
  if (len == 0) return LONG2NUM(0);
24
23
  if (len > IOV_MAX) len = IOV_MAX;
25
24
 
26
25
  int fd = rb_io_descriptor(io);
27
- struct iovec *iov = alloca(len * sizeof(struct iovec));
26
+ struct iovec *iov = ALLOCA_N(struct iovec, len);
28
27
 
29
28
  for (long i = 0; i < len; i++) {
30
29
  VALUE str = RARRAY_AREF(strings, i);
@@ -43,8 +42,9 @@ static VALUE raptor_native_writev_nonblock(VALUE self, VALUE io, VALUE strings)
43
42
  return LONG2NUM((long)written);
44
43
  }
45
44
 
46
- static VALUE raptor_native_pin_to_cpu(VALUE self, VALUE cpu) {
47
- (void)self;
45
+ // Pins the calling thread to the Nth CPU of its current affinity mask, so
46
+ // worker indexes map onto the CPUs the process is actually allowed to use.
47
+ static VALUE cpu_pin(VALUE self, VALUE cpu) {
48
48
  #ifdef __linux__
49
49
  int worker_index = NUM2INT(cpu);
50
50
  cpu_set_t current;
@@ -55,7 +55,10 @@ static VALUE raptor_native_pin_to_cpu(VALUE self, VALUE cpu) {
55
55
  int seen = 0;
56
56
  for (int candidate = 0; candidate < CPU_SETSIZE; candidate++) {
57
57
  if (!CPU_ISSET(candidate, &current)) continue;
58
- if (seen == worker_index) { cpu_id = candidate; break; }
58
+ if (seen == worker_index) {
59
+ cpu_id = candidate;
60
+ break;
61
+ }
59
62
  seen++;
60
63
  }
61
64
  if (cpu_id < 0) return Qfalse;
@@ -66,13 +69,11 @@ static VALUE raptor_native_pin_to_cpu(VALUE self, VALUE cpu) {
66
69
  if (sched_setaffinity(0, sizeof(set), &set) < 0) rb_sys_fail("sched_setaffinity");
67
70
  return Qtrue;
68
71
  #else
69
- (void)cpu;
70
72
  return Qfalse;
71
73
  #endif
72
74
  }
73
75
 
74
- static VALUE raptor_native_cpu_count(VALUE self) {
75
- (void)self;
76
+ static VALUE cpu_count(VALUE self) {
76
77
  #ifdef __linux__
77
78
  cpu_set_t set;
78
79
  CPU_ZERO(&set);
@@ -83,8 +84,24 @@ static VALUE raptor_native_cpu_count(VALUE self) {
83
84
  #endif
84
85
  }
85
86
 
86
- static VALUE raptor_native_enable_subreaper(VALUE self) {
87
- (void)self;
87
+ #ifdef __linux__
88
+ static void *cpu_yield_without_gvl(void *unused) {
89
+ sched_yield();
90
+ return NULL;
91
+ }
92
+ #endif
93
+
94
+ // Thread.pass cannot yield to native threads that have not rejoined
95
+ // Ruby's ready queue.
96
+ static VALUE cpu_yield(VALUE self) {
97
+ rb_thread_schedule();
98
+ #ifdef __linux__
99
+ rb_thread_call_without_gvl(cpu_yield_without_gvl, NULL, NULL, NULL);
100
+ #endif
101
+ return Qnil;
102
+ }
103
+
104
+ static VALUE subreaper_enable(VALUE self) {
88
105
  #if defined(__linux__) && defined(PR_SET_CHILD_SUBREAPER)
89
106
  if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) < 0) rb_sys_fail("prctl");
90
107
  return Qtrue;
@@ -96,18 +113,19 @@ static VALUE raptor_native_enable_subreaper(VALUE self) {
96
113
  RUBY_FUNC_EXPORTED void Init_raptor_native(void) {
97
114
  rb_ext_ractor_safe(true);
98
115
 
116
+ rb_global_variable(&eEAGAINWaitWritable);
117
+ eEAGAINWaitWritable = rb_const_get(rb_cIO, rb_intern("EAGAINWaitWritable"));
118
+
99
119
  VALUE mRaptor = rb_define_module("Raptor");
100
120
 
101
121
  VALUE mVectorIO = rb_define_module_under(mRaptor, "VectorIO");
102
- rb_define_singleton_method(mVectorIO, "writev_nonblock", raptor_native_writev_nonblock, 2);
122
+ rb_define_singleton_method(mVectorIO, "writev_nonblock", vector_io_writev_nonblock, 2);
103
123
 
104
124
  VALUE mCPU = rb_define_module_under(mRaptor, "CPU");
105
- rb_define_singleton_method(mCPU, "pin", raptor_native_pin_to_cpu, 1);
106
- rb_define_singleton_method(mCPU, "count", raptor_native_cpu_count, 0);
125
+ rb_define_singleton_method(mCPU, "pin", cpu_pin, 1);
126
+ rb_define_singleton_method(mCPU, "count", cpu_count, 0);
127
+ rb_define_singleton_method(mCPU, "yield", cpu_yield, 0);
107
128
 
108
129
  VALUE mSubreaper = rb_define_module_under(mRaptor, "Subreaper");
109
- rb_define_singleton_method(mSubreaper, "enable", raptor_native_enable_subreaper, 0);
110
-
111
- eEAGAINWaitWritable = rb_const_get(rb_cIO, rb_intern("EAGAINWaitWritable"));
112
- rb_global_variable(&eEAGAINWaitWritable);
130
+ rb_define_singleton_method(mSubreaper, "enable", subreaper_enable, 0);
113
131
  }
@@ -1,8 +1,6 @@
1
1
  # rbs_inline: enabled
2
2
  # frozen_string_literal: true
3
3
 
4
- require "concurrent/utility/processor_counter"
5
-
6
4
  module Rackup
7
5
  module Handler
8
6
  # Rack handler for booting Raptor through Rackup, `rails server`, or any
@@ -18,7 +16,8 @@ module Rackup
18
16
  #
19
17
  # @param app [#call] the Rack application to serve
20
18
  # @param options [Hash] handler options provided by Rackup or the host
21
- # @yield [cluster] the cluster instance, before it starts running
19
+ # @yield before the cluster starts running
20
+ # @yieldparam cluster [::Raptor::Cluster] the cluster instance
22
21
  # @return [void]
23
22
  #
24
23
  # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
@@ -62,7 +61,7 @@ module Rackup
62
61
  def self.build_cluster_options(app, options)
63
62
  defaults = DEFAULT_OPTIONS.dup
64
63
 
65
- if user_supplied_options = options.delete(:user_supplied_options)
64
+ if (user_supplied_options = options.delete(:user_supplied_options))
66
65
  (options.keys - user_supplied_options).each do |key|
67
66
  defaults[key] = options.delete(key)
68
67
  end
@@ -71,7 +70,9 @@ module Rackup
71
70
  cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
72
71
  config_path = options[:Config] || ::Raptor::CLI.default_config_path
73
72
  config = config_path ? ::Raptor::CLI.load_config_file(config_path) : {}
74
- threads = (options[:Threads] || ENV["RAPTOR_THREADS"] || config[:threads] || cli_defaults[:threads]).to_i
73
+ environment_workers = Integer(ENV["RAPTOR_WORKERS"], 10) if ENV["RAPTOR_WORKERS"]
74
+ environment_threads = Integer(ENV["RAPTOR_THREADS"], 10) if ENV["RAPTOR_THREADS"]
75
+ threads = (options[:Threads] || environment_threads || config[:threads] || cli_defaults[:threads]).to_i
75
76
  max_threads = options[:MaxThreads] || ENV["RAPTOR_MAX_THREADS"] || config.fetch(:max_threads, cli_defaults[:max_threads])
76
77
 
77
78
  result = {
@@ -81,13 +82,13 @@ module Rackup
81
82
  config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
82
83
  end,
83
84
  socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
84
- drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
85
- workers: (options[:Workers] || ENV["RAPTOR_WORKERS"] || config[:workers] || Concurrent.available_processor_count).to_i,
85
+ drain_accept_queue: config.fetch(:drain_accept_queue, cli_defaults[:drain_accept_queue]),
86
+ workers: (options[:Workers] || environment_workers || config[:workers] || cli_defaults[:workers]).to_i,
86
87
  threads: threads,
87
88
  max_threads: ::Raptor::CLI.parse_max_threads(max_threads, threads: threads),
88
- cpu_affinity: config.key?(:cpu_affinity) ? config[:cpu_affinity] : cli_defaults[:cpu_affinity],
89
- clean_thread_locals: config.key?(:clean_thread_locals) ? config[:clean_thread_locals] : cli_defaults[:clean_thread_locals],
90
- clean_fiber_locals: config.key?(:clean_fiber_locals) ? config[:clean_fiber_locals] : cli_defaults[:clean_fiber_locals],
89
+ cpu_affinity: config.fetch(:cpu_affinity, cli_defaults[:cpu_affinity]),
90
+ clean_thread_locals: config.fetch(:clean_thread_locals, cli_defaults[:clean_thread_locals]),
91
+ clean_fiber_locals: config.fetch(:clean_fiber_locals, cli_defaults[:clean_fiber_locals]),
91
92
  app: app
92
93
  }
93
94
  result[:rackup] = config[:rackup] if config.key?(:rackup)
@@ -105,7 +106,7 @@ module Rackup
105
106
  result[:before_worker_boot] = config.fetch(:before_worker_boot, cli_defaults[:before_worker_boot])
106
107
  result[:before_worker_shutdown] = config.fetch(:before_worker_shutdown, cli_defaults[:before_worker_shutdown])
107
108
  result[:before_refork] = config.fetch(:before_refork, cli_defaults[:before_refork])
108
- result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
109
+ result[:stats_file] = config.fetch(:stats_file, cli_defaults[:stats_file])
109
110
  result[:control_url] = config[:control_url] if config.key?(:control_url)
110
111
  result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
111
112
  result[:stdout_file] = config[:stdout_file] if config.key?(:stdout_file)
data/lib/raptor/binder.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # rbs_inline: enabled
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "openssl"
4
5
  require "socket"
5
6
  require "uri"
6
7
 
@@ -56,17 +57,17 @@ module Raptor
56
57
  # Returns the array of bind URIs.
57
58
  #
58
59
  # @return [Array<String>]
59
- attr_reader :bind_uris
60
+ attr_reader :bind_uris #: Array[String]
60
61
 
61
62
  # Returns the kernel `listen()` queue depth for TCP/SSL listeners.
62
63
  #
63
64
  # @return [Integer]
64
- attr_reader :socket_backlog
65
+ attr_reader :socket_backlog #: Integer
65
66
 
66
67
  # Returns the array of listening sockets.
67
68
  #
68
69
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
69
- attr_reader :listeners
70
+ attr_reader :listeners #: Array[TCPServer | UNIXServer | SslListener | H2cListener]
70
71
 
71
72
  # Creates a new Binder and binds each URI. `localhost` expands to both
72
73
  # IPv4 and IPv6 loopback; when `inherited_fds` supplies file descriptors
@@ -113,8 +114,8 @@ module Raptor
113
114
  end
114
115
  end
115
116
 
116
- # Returns the port of the first TCP or SSL listener, or 0 when none
117
- # is configured.
117
+ # Returns the port of the first TCP, SSL, or cleartext HTTP/2 listener,
118
+ # or 0 when none is configured.
118
119
  #
119
120
  # @return [Integer]
120
121
  #
@@ -185,7 +186,7 @@ module Raptor
185
186
 
186
187
  # Creates fresh listeners for the given URI.
187
188
  #
188
- # @param uri [URI] the parsed bind URI
189
+ # @param uri [URI::Generic] the parsed bind URI
189
190
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
190
191
  # @raise [UnknownBindSchemeError] if the URI scheme is not supported
191
192
  #
@@ -208,7 +209,7 @@ module Raptor
208
209
  # Reconstructs listeners for the given URI from inherited file
209
210
  # descriptors.
210
211
  #
211
- # @param uri [URI] the parsed bind URI the FDs were bound to
212
+ # @param uri [URI::Generic] the parsed bind URI the FDs were bound to
212
213
  # @param filenos [Array<Integer>] file descriptors to wrap
213
214
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
214
215
  # @raise [UnknownBindSchemeError] if the URI scheme is not supported
@@ -240,7 +241,7 @@ module Raptor
240
241
  # @rbs (String? host, Integer? port) -> Array[TCPServer]
241
242
  def create_tcp_listeners(host, port)
242
243
  if host == "localhost"
243
- return loopback_addresses.map { |address| create_tcp_listeners(address, port) }.flatten
244
+ return loopback_addresses.flat_map { |address| create_tcp_listeners(address, port) }
244
245
  end
245
246
 
246
247
  host = host[1..-2] if host&.start_with?("[")
@@ -250,6 +251,7 @@ module Raptor
250
251
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
251
252
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
252
253
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
254
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_DEFER_ACCEPT, 1) if Socket.const_defined?(:TCP_DEFER_ACCEPT)
253
255
  socket.bind(addrinfo)
254
256
  socket.listen(@socket_backlog)
255
257
 
@@ -299,7 +301,7 @@ module Raptor
299
301
  #
300
302
  # @param host [String, nil] hostname or IP address to bind to
301
303
  # @param port [Integer, nil] port number to bind to
302
- # @param ssl_params [Hash<String, String>] SSL options (`cert` and `key` file paths)
304
+ # @param ssl_params [Hash{String => String}] SSL options (`cert` and `key` file paths)
303
305
  # @return [Array<SslListener>] array containing the created SSL listener(s)
304
306
  #
305
307
  # @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
@@ -312,13 +314,11 @@ module Raptor
312
314
  # Builds a frozen `OpenSSL::SSL::SSLContext` configured for HTTP/2 and
313
315
  # HTTP/1.1 ALPN negotiation.
314
316
  #
315
- # @param ssl_params [Hash<String, String>] SSL options ("cert" and "key" paths)
317
+ # @param ssl_params [Hash{String => String}] SSL options (`cert` and `key` file paths)
316
318
  # @return [OpenSSL::SSL::SSLContext]
317
319
  #
318
320
  # @rbs (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext
319
321
  def build_ssl_context(ssl_params)
320
- require "openssl"
321
-
322
322
  OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
323
323
  ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
324
324
  ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
data/lib/raptor/cli.rb CHANGED
@@ -7,6 +7,7 @@ require "optparse"
7
7
  require "concurrent/utility/processor_counter"
8
8
 
9
9
  require_relative "cluster"
10
+ require_relative "version"
10
11
 
11
12
  module Raptor
12
13
  # Command-line interface for the Raptor web server.
@@ -48,18 +49,18 @@ module Raptor
48
49
  write_timeout: 5,
49
50
  max_body_size: nil,
50
51
  body_spool_threshold: 1024 * 1024
51
- },
52
+ }.freeze,
52
53
  http1: {
53
54
  ractors: nil,
54
55
  persistent_data_timeout: 65,
55
56
  max_keepalive_requests: 1000
56
- },
57
+ }.freeze,
57
58
  http2: {
58
59
  ractors: nil,
59
60
  max_concurrent_streams: 100,
60
61
  keepalive_interval: 10,
61
62
  keepalive_timeout: 5
62
- },
63
+ }.freeze,
63
64
  worker_boot_timeout: 60,
64
65
  worker_timeout: 60,
65
66
  worker_drain_timeout: 25,
@@ -74,7 +75,7 @@ module Raptor
74
75
  pid_file: nil,
75
76
  stdout_file: nil,
76
77
  stderr_file: nil,
77
- access_log_file: nil,
78
+ access_log_file: nil
78
79
  }.freeze
79
80
 
80
81
  DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
@@ -131,17 +132,17 @@ module Raptor
131
132
  max_threads
132
133
  end
133
134
 
134
- # @rbs @command: Symbol
135
135
  # @rbs @options: Hash[Symbol, untyped]
136
+ # @rbs @command: Symbol
136
137
  # @rbs @parser: OptionParser
137
138
 
138
- # Creates a new CLI instance from `argv`. A rackup file may be given
139
- # as the first positional argument; every other value is parsed as a
140
- # flag.
139
+ # Creates a new CLI from `argv`. A rackup file may be given as the
140
+ # first positional argument; every other value is parsed as a flag.
141
141
  #
142
142
  # @param argv [Array<String>] command-line arguments to parse
143
143
  # @return [void]
144
144
  # @raise [OptionParser::ParseError] if invalid options are provided
145
+ # @raise [ArgumentError] if the config file or max threads value is invalid
145
146
  #
146
147
  # @rbs (Array[String] argv) -> void
147
148
  def initialize(argv)
@@ -203,7 +204,7 @@ module Raptor
203
204
  end
204
205
  end
205
206
 
206
- # Returns the path from a `-c`/`--config` flag in `argv`, recognising
207
+ # Returns the path from a `-c`/`--config` flag in `argv`, recognizing
207
208
  # all four OptionParser-accepted forms (`-c PATH`, `-cPATH`,
208
209
  # `--config PATH`, `--config=PATH`).
209
210
  #
@@ -258,6 +259,8 @@ module Raptor
258
259
  #
259
260
  # @rbs () -> OptionParser
260
261
  def create_parser
262
+ cli_binds = []
263
+
261
264
  OptionParser.new do |opts|
262
265
  opts.banner = "Usage: raptor [options] [rackup file]"
263
266
 
@@ -266,11 +269,8 @@ module Raptor
266
269
  end
267
270
 
268
271
  opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
269
- if @options[:binds].equal?(DEFAULT_OPTIONS[:binds])
270
- @options[:binds] = [bind]
271
- else
272
- @options[:binds] << bind
273
- end
272
+ cli_binds << bind
273
+ @options[:binds] = cli_binds
274
274
  end
275
275
 
276
276
  opts.on("--socket-backlog NUM", Integer, "Socket listen backlog (default: 1024)") do |num|
@@ -333,7 +333,7 @@ module Raptor
333
333
  @options[:connection][:body_spool_threshold] = bytes
334
334
  end
335
335
 
336
- opts.on("--http1-ractors NUM", Integer, "Number of HTTP/1.1 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..3)") do |num|
336
+ opts.on("--http1-ractors NUM", Integer, "Number of HTTP/1.1 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..#{Cluster::HTTP1_RACTOR_COUNT_CAP})") do |num|
337
337
  @options[:http1][:ractors] = num
338
338
  end
339
339
 
@@ -345,7 +345,7 @@ module Raptor
345
345
  @options[:http1][:max_keepalive_requests] = num
346
346
  end
347
347
 
348
- opts.on("--http2-ractors NUM", Integer, "Number of HTTP/2 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..2)") do |num|
348
+ opts.on("--http2-ractors NUM", Integer, "Number of HTTP/2 pipeline ractors per worker (default: `round(cores / workers)`, clamped to 1..#{Cluster::HTTP2_RACTOR_COUNT_CAP})") do |num|
349
349
  @options[:http2][:ractors] = num
350
350
  end
351
351
 
@@ -377,7 +377,7 @@ module Raptor
377
377
  @options[:worker_shutdown_timeout] = timeout
378
378
  end
379
379
 
380
- opts.on("--refork-after NUM", Integer, "Refork workers from a warmed source after any worker crosses NUM requests; 0 disables (default: 1000)") do |num|
380
+ opts.on("--refork-after NUM", Integer, "Refork workers from a warmed source after any worker crosses NUM requests; 0 disables (default: 1000 on Linux, off elsewhere)") do |num|
381
381
  @options[:refork_after] = num
382
382
  end
383
383
 
@@ -411,7 +411,7 @@ module Raptor
411
411
  end
412
412
 
413
413
  opts.on("-v", "--version", "Show version") do
414
- puts Raptor::VERSION
414
+ puts VERSION
415
415
  exit
416
416
  end
417
417
  end
@@ -9,16 +9,18 @@ require "concurrent/utility/processor_counter"
9
9
  require "rack/builder"
10
10
  require "ractor-pool"
11
11
 
12
- require_relative "log"
13
12
  require_relative "binder"
14
13
  require_relative "control_server"
15
- require_relative "reactor"
16
- require_relative "server"
17
- require_relative "reuseport_bpf"
18
14
  require_relative "http1"
19
15
  require_relative "http2"
16
+ require_relative "log"
17
+ require_relative "raptor_native"
18
+ require_relative "reactor"
19
+ require_relative "reuseport_bpf"
20
+ require_relative "server"
20
21
  require_relative "stats"
21
22
  require_relative "systemd"
23
+ require_relative "version"
22
24
 
23
25
  module Raptor
24
26
  # Forks and supervises worker processes. Handles graceful shutdown on
@@ -100,7 +102,7 @@ module Raptor
100
102
  # @rbs @access_log_io: IO?
101
103
  # @rbs @launch_command: String?
102
104
  # @rbs @launch_argv: Array[String]?
103
- # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
105
+ # @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
104
106
  # @rbs @binder: Binder
105
107
  # @rbs @server_port: Integer
106
108
  # @rbs @app: untyped
@@ -115,7 +117,7 @@ module Raptor
115
117
  # @rbs @hot_restart_requested: bool
116
118
  # @rbs @refork_thresholds: Array[Integer]
117
119
  # @rbs @refork_requested: bool
118
- # @rbs @refork_threshold_idx: Integer
120
+ # @rbs @refork_threshold_index: Integer
119
121
  # @rbs @seed_pid: Integer?
120
122
  # @rbs @seed_ready: bool
121
123
  # @rbs @seed_vacated_index: Integer?
@@ -200,7 +202,7 @@ module Raptor
200
202
 
201
203
  Dir.chdir(options[:chdir]) if options[:chdir]
202
204
 
203
- inherited_fds = if raw = ENV.delete(INHERITED_FDS_ENV)
205
+ inherited_fds = if (raw = ENV.delete(INHERITED_FDS_ENV))
204
206
  JSON.parse(raw)
205
207
  elsif (systemd_fds = Systemd.listen_fds).any?
206
208
  Systemd.clear_listen_env
@@ -225,7 +227,7 @@ module Raptor
225
227
 
226
228
  @refork_thresholds = normalize_refork_thresholds(options[:refork_after])
227
229
  @refork_requested = false
228
- @refork_threshold_idx = 0
230
+ @refork_threshold_index = 0
229
231
  @seed_pid = nil
230
232
  @seed_ready = false
231
233
  @seed_vacated_index = nil
@@ -261,7 +263,7 @@ module Raptor
261
263
 
262
264
  File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
263
265
 
264
- @bpf_active = ReuseportBPF.setup(@worker_count)
266
+ @bpf_active = ReuseportBPF.setup(@worker_count, @binder.listeners.grep(TCPServer))
265
267
 
266
268
  @control_server&.bind
267
269
  @worker_count.times { |index| spawn_worker(index) }
@@ -310,9 +312,11 @@ module Raptor
310
312
  @stats.all
311
313
  end
312
314
 
313
- # Returns cluster statistics for the control server.
314
- # For an adaptive pool, `max_threads` is the number of threads currently
315
- # running so utilization reflects the capacity available at that moment.
315
+ private
316
+
317
+ # Returns cluster statistics for the control server. For an adaptive
318
+ # pool, `max_threads` is the number of threads currently running so
319
+ # utilization reflects the capacity available at that moment.
316
320
  #
317
321
  # @return [Hash] cluster statistics
318
322
  #
@@ -354,11 +358,9 @@ module Raptor
354
358
  }
355
359
  end
356
360
 
357
- private
358
-
359
- # @rbs (Float timestamp) -> String
360
- def timestamp(timestamp)
361
- Time.at(timestamp).utc.iso8601(6)
361
+ # @rbs (Numeric time) -> String
362
+ def timestamp(time)
363
+ Time.at(time).utc.iso8601(6)
362
364
  end
363
365
 
364
366
  # Returns the inherited-FDs hash for a systemd socket-activation handoff,
@@ -449,7 +451,7 @@ module Raptor
449
451
  def pid_alive?(pid)
450
452
  Process.kill(0, pid)
451
453
  true
452
- rescue Errno::ESRCH, Errno::ECHILD
454
+ rescue Errno::ESRCH
453
455
  false
454
456
  rescue Errno::EPERM
455
457
  true
@@ -513,14 +515,14 @@ module Raptor
513
515
  return unless candidate
514
516
 
515
517
  candidate_index, candidate_requests = candidate
516
- threshold = @refork_thresholds[@refork_threshold_idx]
518
+ threshold = @refork_thresholds[@refork_threshold_index]
517
519
 
518
520
  if @refork_requested
519
521
  @refork_requested = false
520
522
  elsif !threshold || candidate_requests < threshold
521
523
  return
522
524
  else
523
- @refork_threshold_idx += 1
525
+ @refork_threshold_index += 1
524
526
  end
525
527
 
526
528
  promote_worker_to_seed(candidate_index)
@@ -561,7 +563,6 @@ module Raptor
561
563
 
562
564
  retire_current_seed
563
565
  Log.info "Promoting worker #{index} to seed for phased refork"
564
- ReuseportBPF.mark_unavailable(index) if @bpf_active
565
566
  Process.kill("URG", pid) rescue nil
566
567
  @workers.delete(index)
567
568
  @seed_pid = pid
@@ -800,7 +801,7 @@ module Raptor
800
801
  trap("USR2", "IGNORE")
801
802
  trap("URG") { promote_to_seed = true } if @fork_r
802
803
 
803
- Raptor::CPU.pin(index) if @cpu_affinity && Raptor::CPU.count >= @worker_count
804
+ CPU.pin(index) if @cpu_affinity && CPU.count >= @worker_count
804
805
 
805
806
  started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
806
807
  request_count = 0
@@ -813,7 +814,7 @@ module Raptor
813
814
  backlog: 0,
814
815
  busy_threads: 0,
815
816
  thread_capacity: @thread_count,
816
- started_at:,
817
+ started_at: started_at,
817
818
  last_checkin: started_at,
818
819
  booted: false
819
820
  )
@@ -872,6 +873,7 @@ module Raptor
872
873
  end
873
874
 
874
875
  reactor = Reactor.new(
876
+ http1,
875
877
  http1_ractor_pool,
876
878
  http2_ractor_pool,
877
879
  thread_pool,
@@ -881,24 +883,21 @@ module Raptor
881
883
  )
882
884
  reactor_thread = reactor.run
883
885
 
884
- worker_listeners = if @bpf_active
885
- bpf_listeners = ReuseportBPF.create_worker_listeners(@binder.bind_uris, index, @binder.socket_backlog)
886
+ bpf_listeners = if @bpf_active
886
887
  ReuseportBPF.enable_load_reporting(index)
887
- non_tcp_listeners = @binder.listeners.reject { |listener| listener.is_a?(TCPServer) }
888
- bpf_listeners + non_tcp_listeners
888
+ ReuseportBPF.create_worker_listeners(index, @binder.socket_backlog)
889
889
  else
890
- @binder.listeners
890
+ []
891
891
  end
892
892
 
893
893
  server = Server.new(
894
- @binder,
895
894
  reactor,
896
895
  thread_pool,
897
896
  http1,
898
897
  http2,
899
898
  connection_options: @connection_options,
900
899
  drain_accept_queue: @drain_accept_queue,
901
- listeners: worker_listeners,
900
+ listeners: bpf_listeners + @binder.listeners,
902
901
  worker_index: (index if @bpf_active)
903
902
  )
904
903
  server_thread = server.run
@@ -919,7 +918,7 @@ module Raptor
919
918
  backlog: reactor.backlog,
920
919
  busy_threads: thread_pool.active_count,
921
920
  thread_capacity: thread_pool.size,
922
- started_at:,
921
+ started_at: started_at,
923
922
  last_checkin: Process.clock_gettime(Process::CLOCK_REALTIME),
924
923
  booted: true
925
924
  )
@@ -944,6 +943,8 @@ module Raptor
944
943
  end
945
944
 
946
945
  server_thread.join
946
+ # A seed never accepts again, and the workers it forks bind their own.
947
+ bpf_listeners.each(&:close) if promote_to_seed
947
948
  http1.shutdown
948
949
  http2.shutdown(reactor)
949
950
  reactor.drain_detached_bodies(@worker_drain_timeout)
@@ -972,11 +973,6 @@ module Raptor
972
973
  trap("TERM") { seed_shutdown = true }
973
974
  trap("URG", "IGNORE")
974
975
 
975
- child_pids = []
976
- trap("CHLD") do
977
- child_pids.reject! { Process.wait(_1, Process::WNOHANG) rescue true }
978
- end
979
-
980
976
  @resp_w.write([0].pack("L"))
981
977
 
982
978
  until seed_shutdown
@@ -986,9 +982,15 @@ module Raptor
986
982
  break unless bytes.is_a?(String) && bytes.bytesize == 8
987
983
 
988
984
  slot_index, child_phase = bytes.unpack("LL")
989
- pid = fork { run_worker(slot_index, child_phase) }
990
- child_pids << pid
991
- @resp_w.write([pid].pack("L")) rescue nil
985
+ # Forks through a short-lived intermediate process so the worker
986
+ # reparents to the subreaper master, which supervises and reaps it
987
+ # like a worker it forked itself.
988
+ intermediate_pid = fork do
989
+ pid = fork { run_worker(slot_index, child_phase) }
990
+ @resp_w.write([pid].pack("L")) rescue nil
991
+ exit!
992
+ end
993
+ Process.wait(intermediate_pid)
992
994
  end
993
995
  rescue Errno::EPIPE, IOError
994
996
  end
@@ -1002,16 +1004,16 @@ module Raptor
1002
1004
  #
1003
1005
  # @rbs (AtomicThreadPool thread_pool) -> void
1004
1006
  def drain_thread_pool(thread_pool)
1005
- drain = Thread.new do
1007
+ drain_thread = Thread.new do
1006
1008
  Thread.current.name = "Thread Pool Drain"
1007
1009
 
1008
1010
  thread_pool.shutdown
1009
1011
  end
1010
- return if drain.join(@worker_drain_timeout)
1012
+ return if drain_thread.join(@worker_drain_timeout)
1011
1013
 
1012
1014
  Log.warn "Force-killing in-flight app threads after #{@worker_drain_timeout}s drain timeout"
1013
1015
  thread_pool.instance_variable_get(:@threads).each(&:kill)
1014
- drain.join
1016
+ drain_thread.join
1015
1017
  end
1016
1018
 
1017
1019
  # Returns a human-readable description of how a process exited.
@@ -1036,8 +1038,6 @@ module Raptor
1036
1038
  #
1037
1039
  # @rbs () -> void
1038
1040
  def shutdown
1039
- return if @shutdown
1040
-
1041
1041
  @shutdown = true
1042
1042
  end
1043
1043