raptor 0.10.0 → 0.12.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.
data/lib/raptor/http1.rb CHANGED
@@ -58,6 +58,22 @@ module Raptor
58
58
 
59
59
  ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
60
60
  ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
61
+ CHUNK_SIZE_REGEX = /\A[0-9A-Fa-f]+\z/
62
+
63
+ # Returns true when an HTTP/1.1 request lacks a valid `Host` header per
64
+ # RFC 9112 section 3.2, where a valid value is a non-empty single-value
65
+ # line.
66
+ #
67
+ # @param env [Hash] the Rack environment after header parsing
68
+ # @return [Boolean]
69
+ #
70
+ # @rbs (Hash[String, untyped] env) -> bool
71
+ def self.invalid_host?(env)
72
+ return false unless env[Rack::SERVER_PROTOCOL] == HTTP_11
73
+
74
+ http_host = env[Rack::HTTP_HOST]
75
+ !http_host || http_host.empty? || http_host.include?(",")
76
+ end
61
77
 
62
78
  # Returns true when the message framing shows a request-smuggling vector
63
79
  # per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
@@ -89,9 +105,10 @@ module Raptor
89
105
  # Decodes a chunked transfer-encoded body buffer.
90
106
  #
91
107
  # Returns the decoded bytes and a state symbol: `:complete` when the
92
- # terminating zero-length chunk was found, `:too_large` when the decoded
93
- # size would exceed `max_size`, `:malformed` when chunk framing overhead
94
- # exceeds `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
108
+ # terminating zero-length chunk and trailer section were fully consumed,
109
+ # `:too_large` when the decoded size would exceed `max_size`, `:malformed`
110
+ # when a chunk-size line is not valid hex or chunk framing overhead exceeds
111
+ # `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
95
112
  #
96
113
  # @param buffer [String] the raw body buffer to decode
97
114
  # @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
@@ -107,8 +124,24 @@ module Raptor
107
124
  crlf = buffer.index("\r\n", offset)
108
125
  return [decoded, :incomplete] unless crlf
109
126
 
110
- chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
111
- return [decoded, :complete] if chunk_size == 0
127
+ size_line = buffer.byteslice(offset, crlf - offset)
128
+ semicolon = size_line.index(";")
129
+ size_part = semicolon ? size_line.byteslice(0, semicolon) : size_line
130
+ return [decoded, :malformed] unless size_part.match?(CHUNK_SIZE_REGEX)
131
+
132
+ chunk_size = size_part.to_i(16)
133
+
134
+ if chunk_size == 0
135
+ trailer_offset = crlf + 2
136
+ loop do
137
+ trailer_crlf = buffer.index("\r\n", trailer_offset)
138
+ return [decoded, :incomplete] unless trailer_crlf
139
+ return [decoded, :complete] if trailer_crlf == trailer_offset
140
+
141
+ trailer_offset = trailer_crlf + 2
142
+ end
143
+ end
144
+
112
145
  return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
113
146
 
114
147
  overhead += (crlf - offset) + 4
@@ -240,7 +273,8 @@ module Raptor
240
273
  buffer << socket.read_nonblock(socket.pending)
241
274
  end
242
275
 
243
- parser = HttpParser.new
276
+ parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
277
+ parser.reset
244
278
  env = @env_template.dup
245
279
  nread = begin
246
280
  parser.execute(env, buffer, 0)
@@ -254,7 +288,7 @@ module Raptor
254
288
  if !parser.finished?
255
289
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
256
290
  return
257
- elsif Http1.request_smuggling?(env)
291
+ elsif Http1.invalid_host?(env) || Http1.request_smuggling?(env)
258
292
  reject_malformed(socket)
259
293
  return
260
294
  elsif parser.has_body?
@@ -265,7 +299,7 @@ module Raptor
265
299
 
266
300
  body = buffer.byteslice(nread..-1) || ""
267
301
 
268
- if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
302
+ if parser.chunked?
269
303
  body, chunked_state = Http1.decode_chunked(body, @max_body_size)
270
304
  case chunked_state
271
305
  when :complete
@@ -322,14 +356,14 @@ module Raptor
322
356
  parse_data[:parse_count] += 1
323
357
 
324
358
  message = if parser.finished?
325
- if Raptor::Http1.request_smuggling?(env)
359
+ if Raptor::Http1.invalid_host?(env) || Raptor::Http1.request_smuggling?(env)
326
360
  data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
327
361
  elsif parser.has_body?
328
362
  body_buffer = data[:buffer].byteslice(nread..-1) || ""
329
363
 
330
364
  if max_body_size && parser.content_length > max_body_size
331
365
  data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
332
- elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
366
+ elsif parser.chunked?
333
367
  decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
334
368
 
335
369
  case chunked_state
@@ -571,7 +605,8 @@ module Raptor
571
605
  buffer << socket.read_nonblock(socket.pending)
572
606
  end
573
607
 
574
- parser = HttpParser.new
608
+ parser = (Thread.current[:raptor_http_parser] ||= HttpParser.new)
609
+ parser.reset
575
610
  env = @env_template.dup
576
611
  nread = begin
577
612
  parser.execute(env, buffer, 0)
@@ -588,8 +623,7 @@ module Raptor
588
623
  elsif parser.has_body?
589
624
  body = buffer.byteslice(nread..-1) || ""
590
625
 
591
- chunked = env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
592
- if chunked || parser.content_length > body.bytesize
626
+ if parser.chunked? || parser.content_length > body.bytesize
593
627
  fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
594
628
  return
595
629
  end
@@ -597,7 +631,7 @@ module Raptor
597
631
 
598
632
  request_count += 1
599
633
 
600
- if thread_pool.queue_size > 0
634
+ if thread_pool.queue_size >= thread_pool.size
601
635
  thread_pool << proc do
602
636
  process_client(
603
637
  socket,
@@ -718,8 +752,23 @@ module Raptor
718
752
  send_early_hints(socket, hints) rescue nil
719
753
  end
720
754
 
721
- env[Rack::PATH_INFO] = env.delete(Rack::REQUEST_PATH) if env.key?(Rack::REQUEST_PATH)
722
- env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
755
+ unless env.key?(Rack::PATH_INFO)
756
+ request_uri = env[Http::REQUEST_URI]
757
+ scheme_end = request_uri&.index("://")
758
+ if scheme_end
759
+ authority_end = request_uri.index("/", scheme_end + 3) || request_uri.bytesize
760
+ path_and_query = request_uri.byteslice(authority_end..-1) || ""
761
+ if query_delim = path_and_query.index("?")
762
+ env[Rack::PATH_INFO] = query_delim.zero? ? "/" : path_and_query.byteslice(0, query_delim)
763
+ env[Rack::QUERY_STRING] = path_and_query.byteslice(query_delim + 1..-1)
764
+ else
765
+ env[Rack::PATH_INFO] = path_and_query.empty? ? "/" : path_and_query
766
+ end
767
+ else
768
+ env[Rack::PATH_INFO] = ""
769
+ end
770
+ end
771
+
723
772
  if (content_length = parse_data[:content_length]).positive?
724
773
  env[Http::CONTENT_LENGTH] = content_length.to_s
725
774
  end
@@ -976,7 +1025,7 @@ module Raptor
976
1025
  #
977
1026
  # @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, ^(TCPSocket) -> void response_hijack) -> void
978
1027
  def write_hijacked_response(socket, response, headers, response_hijack)
979
- response << format_headers(headers)
1028
+ format_headers(response, headers)
980
1029
  response << "\r\n"
981
1030
  socket_write(socket, response)
982
1031
  uncork_socket(socket)
@@ -1001,7 +1050,7 @@ module Raptor
1001
1050
  headers[Rack::CONTENT_LENGTH] = "0" unless headers.key?(Rack::CONTENT_LENGTH) || headers.key?(Rack::TRANSFER_ENCODING)
1002
1051
  end
1003
1052
 
1004
- response << format_headers(headers)
1053
+ format_headers(response, headers)
1005
1054
  response << "\r\n"
1006
1055
  socket_write(socket, response)
1007
1056
  end
@@ -1023,7 +1072,7 @@ module Raptor
1023
1072
  # @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
1024
1073
  def write_full_response(socket, response, headers, body, http_version)
1025
1074
  if body.respond_to?(:call)
1026
- response << format_headers(headers)
1075
+ format_headers(response, headers)
1027
1076
  response << "\r\n"
1028
1077
  socket_write(socket, response)
1029
1078
  uncork_socket(socket)
@@ -1049,7 +1098,7 @@ module Raptor
1049
1098
  headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED
1050
1099
  end
1051
1100
 
1052
- response << format_headers(headers)
1101
+ format_headers(response, headers)
1053
1102
  response << "\r\n"
1054
1103
 
1055
1104
  if body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
@@ -1253,9 +1302,8 @@ module Raptor
1253
1302
  # @param headers [Hash] normalized response headers
1254
1303
  # @return [String] formatted header lines, each ending with CRLF
1255
1304
  #
1256
- # @rbs (Hash[String, String | Array[String]] headers) -> String
1257
- def format_headers(headers)
1258
- result = +""
1305
+ # @rbs (String result, Hash[String, String | Array[String]] headers) -> void
1306
+ def format_headers(result, headers)
1259
1307
  headers.each do |name, value|
1260
1308
  next if illegal_header_key?(name)
1261
1309
 
@@ -1265,7 +1313,6 @@ module Raptor
1265
1313
  append_header_value(result, name, value)
1266
1314
  end
1267
1315
  end
1268
- result
1269
1316
  end
1270
1317
 
1271
1318
  # Appends one or more `name: value` header lines to `result`. Newline-
data/lib/raptor/http2.rb CHANGED
@@ -247,6 +247,39 @@ module Raptor
247
247
  RACK_HEADER_PREFIX = "rack."
248
248
  HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
249
249
 
250
+ REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
251
+ REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
252
+
253
+ # Returns true when a decoded header block violates the HTTP/2 pseudo-header
254
+ # rules from RFC 9113 section 8.3: an unknown pseudo-header, a duplicate
255
+ # pseudo-header, a pseudo-header appearing after any regular header, or a
256
+ # required pseudo-header (`:method`, `:scheme`, `:path`) missing on
257
+ # non-`CONNECT` requests.
258
+ #
259
+ # @param headers [Array<Array(String, String)>] decoded header pairs
260
+ # @return [Boolean]
261
+ #
262
+ # @rbs (Array[[String, String]] headers) -> bool
263
+ def self.invalid_pseudo_headers?(headers)
264
+ seen_pseudo = {}
265
+ seen_regular = false
266
+
267
+ headers.each do |name, _value|
268
+ if name.start_with?(":")
269
+ return true if seen_regular
270
+ return true unless REQUEST_PSEUDO_HEADERS.include?(name)
271
+ return true if seen_pseudo[name]
272
+ seen_pseudo[name] = true
273
+ else
274
+ seen_regular = true
275
+ end
276
+ end
277
+
278
+ return false if seen_pseudo[":method"] && headers.assoc(":method")&.last == "CONNECT"
279
+
280
+ REQUIRED_REQUEST_PSEUDO_HEADERS.any? { |name| !seen_pseudo[name] }
281
+ end
282
+
250
283
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
251
284
  # @rbs @server_port: Integer
252
285
  # @rbs @write_timeout: Integer
@@ -370,7 +403,12 @@ module Raptor
370
403
 
371
404
  if (frame[:flags] & FLAG_END_HEADERS) != 0
372
405
  decoded_headers, hpack_table = parser.parse_headers(header_payload, hpack_table)
373
- streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
406
+ if invalid_pseudo_headers?(decoded_headers)
407
+ streams.delete(stream_id)
408
+ outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
409
+ else
410
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
411
+ end
374
412
  else
375
413
  pending_headers = { stream_id: stream_id, buffer: header_payload, end_stream: end_stream }
376
414
  end
@@ -386,7 +424,12 @@ module Raptor
386
424
  if (frame[:flags] & FLAG_END_HEADERS) != 0
387
425
  stream_id = pending_headers[:stream_id]
388
426
  decoded_headers, hpack_table = parser.parse_headers(pending_headers[:buffer], hpack_table)
389
- streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
427
+ if invalid_pseudo_headers?(decoded_headers)
428
+ streams.delete(stream_id)
429
+ outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
430
+ else
431
+ streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, pending_headers[:end_stream])
432
+ end
390
433
  pending_headers = nil
391
434
  end
392
435
 
@@ -688,7 +731,7 @@ module Raptor
688
731
  next if HOP_BY_HOP_HEADERS.include?(lowered)
689
732
 
690
733
  if value.is_a?(Array)
691
- value.each { |val| header_pairs << [lowered, val.to_s] }
734
+ value.each { |entry| header_pairs << [lowered, entry.to_s] }
692
735
  else
693
736
  header_pairs << [lowered, value.to_s]
694
737
  end
@@ -105,5 +105,18 @@ module Raptor
105
105
  @load_value.setbyte(3, (backlog >> 24) & 0xff)
106
106
  LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
107
107
  end
108
+
109
+ # Marks a worker's slot as unavailable by parking its load at the
110
+ # sentinel maximum so the BPF program's least-loaded pick skips it.
111
+ #
112
+ # @param worker_index [Integer] slot index for the worker to mark
113
+ # @return [void]
114
+ #
115
+ # @rbs (Integer worker_index) -> void
116
+ def self.mark_unavailable(worker_index)
117
+ return unless @loads_fd
118
+
119
+ LibBPFRuby.map_update(@loads_fd, [worker_index + 1].pack("L"), [0xffffffff].pack("L"))
120
+ end
108
121
  end
109
122
  end
data/lib/raptor/server.rb CHANGED
@@ -111,18 +111,26 @@ module Raptor
111
111
  end
112
112
  end
113
113
 
114
- # Gracefully shuts down the server.
115
- #
116
- # Stops accepting new connections and closes all listening sockets. When
117
- # `drain_accept_queue` is enabled, dispatches every connection already in
118
- # the kernel accept queue before closing the listeners.
114
+ # Stops the accept loop and, when `drain_accept_queue` is enabled,
115
+ # dispatches every connection already in the kernel accept queue. Leaves
116
+ # the listening sockets open so they can be handed to replacement workers.
119
117
  #
120
118
  # @return [void]
121
119
  #
122
120
  # @rbs () -> void
123
- def shutdown
121
+ def stop_accepting
124
122
  @running.make_false
125
123
  drain_accept_queue if @drain_accept_queue
124
+ end
125
+
126
+ # Gracefully shuts down the server, stopping the accept loop and closing
127
+ # all listening sockets.
128
+ #
129
+ # @return [void]
130
+ #
131
+ # @rbs () -> void
132
+ def shutdown
133
+ stop_accepting
126
134
  @listeners.each(&:close)
127
135
  end
128
136
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.10.0"
5
+ VERSION = "0.12.0"
6
6
  end
@@ -40,57 +40,85 @@ module Raptor
40
40
  # @rbs (Hash[Symbol, untyped] options) -> void
41
41
  def self.run: (Hash[Symbol, untyped] options) -> void
42
42
 
43
- @http2_options: Hash[Symbol, untyped]
43
+ @bpf_active: bool
44
44
 
45
- @worker_boot_timeout: Integer
45
+ @server_port: Integer
46
46
 
47
- @worker_timeout: Integer
47
+ @binder: Binder
48
48
 
49
- @worker_drain_timeout: Integer
49
+ @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
50
50
 
51
- @worker_shutdown_timeout: Integer
51
+ @launch_argv: Array[String]?
52
52
 
53
- @stats_file: String?
53
+ @launch_command: String?
54
54
 
55
- @pid_file: String?
55
+ @access_log_io: IO?
56
56
 
57
- @stdout_file: String?
57
+ @access_log_file: String?
58
58
 
59
59
  @stderr_file: String?
60
60
 
61
- @access_log_file: String?
61
+ @stdout_file: String?
62
62
 
63
- @access_log_io: IO?
63
+ @pid_file: String?
64
64
 
65
- @launch_command: String?
65
+ @stats_file: String?
66
66
 
67
- @launch_argv: Array[String]?
67
+ @before_refork: Array[Proc]
68
68
 
69
- @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
69
+ @before_worker_shutdown: Array[Proc]
70
70
 
71
- @binder: Binder
71
+ @before_worker_boot: Array[Proc]
72
72
 
73
- @server_port: Integer
73
+ @before_fork: Array[Proc]
74
74
 
75
- @app: untyped
75
+ @worker_shutdown_timeout: Integer
76
76
 
77
- @shutdown: bool
77
+ @worker_drain_timeout: Integer
78
78
 
79
- @workers: Hash[Integer, Integer]
79
+ @worker_timeout: Integer
80
80
 
81
- @timed_out: Set[Integer]
81
+ @worker_boot_timeout: Integer
82
82
 
83
- @stats: Stats
83
+ @resp_w: IO?
84
84
 
85
- @phase: Integer
85
+ @resp_r: IO?
86
86
 
87
- @phased_restart_requested: bool
87
+ @fork_w: IO?
88
88
 
89
- @phased_restarting: bool
89
+ @fork_r: IO?
90
+
91
+ @seed_vacated_index: Integer?
92
+
93
+ @seed_ready: bool
94
+
95
+ @seed_pid: Integer?
96
+
97
+ @refork_threshold_idx: Integer
98
+
99
+ @refork_requested: bool
100
+
101
+ @refork_thresholds: Array[Integer]
90
102
 
91
103
  @hot_restart_requested: bool
92
104
 
93
- @bpf_active: bool
105
+ @phased_restarting: bool
106
+
107
+ @phased_restart_requested: bool
108
+
109
+ @phase: Integer
110
+
111
+ @stats: Stats
112
+
113
+ @timed_out: Set[Integer]
114
+
115
+ @workers: Hash[Integer, Integer]
116
+
117
+ @shutdown: bool
118
+
119
+ @app: untyped
120
+
121
+ @http2_options: Hash[Symbol, untyped]
94
122
 
95
123
  @http1_options: Hash[Symbol, untyped]
96
124
 
@@ -130,6 +158,11 @@ module Raptor
130
158
  # @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
131
159
  # @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
132
160
  # @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
161
+ # @option options [Integer, Array<Integer>, nil] :refork_after request-count threshold(s) at which a warm worker is promoted to a fork source for phased refork; nil or 0 disables. Requires `PR_SET_CHILD_SUBREAPER` (Linux)
162
+ # @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
163
+ # @option options [Array<Proc>] :before_worker_boot procs called in each worker before it begins serving
164
+ # @option options [Array<Proc>] :before_worker_shutdown procs called in each worker before its graceful shutdown
165
+ # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
133
166
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
134
167
  # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
135
168
  # @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
@@ -185,8 +218,18 @@ module Raptor
185
218
  # @rbs (Array[String] bind_uris, Array[Integer] filenos) -> Hash[String, Array[Integer]]
186
219
  def pair_systemd_fds: (Array[String] bind_uris, Array[Integer] filenos) -> Hash[String, Array[Integer]]
187
220
 
188
- # Forks a new worker process and registers it at the given index.
189
- # The worker inherits the cluster's current phase.
221
+ # Normalises the `refork_after` option into a sorted array of positive
222
+ # thresholds. Accepts nil, 0, an Integer, or an Array<Integer>; anything
223
+ # else falls back to an empty array (feature disabled).
224
+ #
225
+ # @param value [Integer, Array<Integer>, nil] the raw option value
226
+ # @return [Array<Integer>]
227
+ #
228
+ # @rbs (untyped value) -> Array[Integer]
229
+ def normalize_refork_thresholds: (untyped value) -> Array[Integer]
230
+
231
+ # Forks a new worker process and registers it at the given index,
232
+ # forking from the seed when one is active and off the master otherwise.
190
233
  #
191
234
  # @param index [Integer] slot index for this worker in the stats region
192
235
  # @return [void]
@@ -194,16 +237,87 @@ module Raptor
194
237
  # @rbs (Integer index) -> void
195
238
  def spawn_worker: (Integer index) -> void
196
239
 
240
+ # Asks the seed to fork a new worker at the given index, returning the
241
+ # child pid, or nil when the seed doesn't respond in time.
242
+ #
243
+ # @param index [Integer] slot index for the new worker
244
+ # @return [Integer, nil] the forked worker's pid, or nil on failure
245
+ #
246
+ # @rbs (Integer index) -> Integer?
247
+ def spawn_worker_via_seed: (Integer index) -> Integer?
248
+
249
+ # Checks whether a process with the given pid is currently alive.
250
+ #
251
+ # @param pid [Integer] the pid to probe
252
+ # @return [Boolean] true if the process exists
253
+ #
254
+ # @rbs (Integer pid) -> bool
255
+ def pid_alive?: (Integer pid) -> bool
256
+
197
257
  # Reaps any worker processes that have exited, respawning each one
198
258
  # unless the cluster is shutting down.
199
259
  #
200
- # @return [Symbol] :no_children when there are no remaining children, otherwise :reaped
260
+ # @return [Symbol] :no_children when nothing is left to supervise, otherwise :reaped
201
261
  #
202
262
  # @rbs () -> Symbol
203
263
  def reap_workers: () -> Symbol
204
264
 
205
- # Stops every worker, escalating from TERM to KILL if any fail to
206
- # exit within `worker_shutdown_timeout`.
265
+ # Records a reaped pid, respawning its worker slot unless the cluster
266
+ # is shutting down. Clears the seed reference when the seed exits.
267
+ #
268
+ # @param pid [Integer] the reaped pid
269
+ # @param status [Process::Status] the exit status
270
+ # @return [void]
271
+ #
272
+ # @rbs (Integer pid, Process::Status status) -> void
273
+ def reap_pid: (Integer pid, Process::Status status) -> void
274
+
275
+ # Promotes the most-experienced worker to a seed and starts a phased
276
+ # refork when the next `refork_after` threshold is crossed or a manual
277
+ # refork was requested via `SIGURG`.
278
+ #
279
+ # @return [void]
280
+ #
281
+ # @rbs () -> void
282
+ def check_refork_trigger: () -> void
283
+
284
+ # Picks the most-experienced booted worker in the current phase,
285
+ # returning its slot index and its request count. Returns nil when
286
+ # no worker qualifies.
287
+ #
288
+ # @return [Array<Integer>, nil]
289
+ #
290
+ # @rbs () -> Array[Integer]?
291
+ def pick_refork_candidate: () -> Array[Integer]?
292
+
293
+ # Retires the current seed and promotes the given worker into its place,
294
+ # queueing a phased refork for the remaining workers.
295
+ #
296
+ # @param index [Integer] slot index of the worker to promote
297
+ # @return [void]
298
+ #
299
+ # @rbs (Integer index) -> void
300
+ def promote_worker_to_seed: (Integer index) -> void
301
+
302
+ # Terminates the currently-active seed process, if any, and waits for
303
+ # it to exit. Its seed-forked workers stay attached to the master and
304
+ # keep serving.
305
+ #
306
+ # @return [void]
307
+ #
308
+ # @rbs () -> void
309
+ def retire_current_seed: () -> void
310
+
311
+ # Records the seed's readiness when its ready marker has arrived.
312
+ # Non-blocking.
313
+ #
314
+ # @return [void]
315
+ #
316
+ # @rbs () -> void
317
+ def poll_seed_ready: () -> void
318
+
319
+ # Stops every worker (and the seed if one is active), escalating
320
+ # from TERM to KILL if any fail to exit within `worker_shutdown_timeout`.
207
321
  #
208
322
  # @return [void]
209
323
  #
@@ -229,6 +343,23 @@ module Raptor
229
343
  # @rbs () -> void
230
344
  def perform_phased_restart: () -> void
231
345
 
346
+ # Blocks until the freshly-promoted seed has signalled readiness,
347
+ # or times out and clears the seed reference. No-op when no seed is
348
+ # being promoted.
349
+ #
350
+ # @return [void]
351
+ #
352
+ # @rbs () -> void
353
+ def wait_for_seed_ready: () -> void
354
+
355
+ # Spawns a replacement worker for the slot the seed vacated when
356
+ # it was promoted, returning the slot index it filled.
357
+ #
358
+ # @return [Integer, nil] the filled slot index, or nil if none was vacated
359
+ #
360
+ # @rbs () -> Integer?
361
+ def fill_vacated_seed_slot: () -> Integer?
362
+
232
363
  # Re-execs the master process with a fresh boot of the same Raptor
233
364
  # invocation, handing the new master its listening sockets so accepted
234
365
  # connections continue to be served across the swap.
@@ -238,11 +369,10 @@ module Raptor
238
369
  # @rbs () -> void
239
370
  def perform_hot_restart: () -> void
240
371
 
241
- # Runs the full server stack inside a worker process.
242
- #
243
- # Sets up and coordinates the reactor, server, ractor pool, thread pool,
244
- # and stats thread, running until a shutdown signal is received or a
245
- # critical component fails.
372
+ # Runs a worker process's full server stack until a shutdown signal is
373
+ # received or a critical component fails. On `SIGURG` the worker drains
374
+ # and transitions into a seed loop that forks replacement workers on
375
+ # master's request.
246
376
  #
247
377
  # @param index [Integer] slot index for this worker in the stats region
248
378
  # @param phase [Integer] the cluster phase this worker was forked at
@@ -251,6 +381,15 @@ module Raptor
251
381
  # @rbs (Integer index, Integer phase) -> void
252
382
  def run_worker: (Integer index, Integer phase) -> void
253
383
 
384
+ # Runs the seed's fork loop, forking a replacement worker for each
385
+ # slot index the master asks for.
386
+ #
387
+ # @param index [Integer] the seed's original slot index
388
+ # @return [void]
389
+ #
390
+ # @rbs (Integer index) -> void
391
+ def run_seed_loop: (Integer index) -> void
392
+
254
393
  # Shuts down the worker's application thread pool, force-killing the
255
394
  # underlying threads if in-flight requests have not finished within
256
395
  # `worker_drain_timeout` seconds.
@@ -15,6 +15,8 @@ module Raptor
15
15
 
16
16
  REMOTE_ADDR: ::String
17
17
 
18
+ REQUEST_URI: ::String
19
+
18
20
  SERVER_SOFTWARE: ::String
19
21
 
20
22
  SERVER_SOFTWARE_VALUE: untyped