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.
- checksums.yaml +4 -4
- data/.buildkite/pipeline.yml +100 -0
- data/CHANGELOG.md +21 -0
- data/README.md +47 -31
- data/docs/raptor-vs-puma.md +104 -75
- data/ext/raptor_bpf/reuseport_select.bpf.c +3 -2
- data/ext/raptor_http/raptor_http.c +28 -3
- data/ext/raptor_native/raptor_native.c +16 -2
- data/lib/raptor/cli.rb +11 -2
- data/lib/raptor/cluster.rb +368 -29
- data/lib/raptor/http.rb +1 -0
- data/lib/raptor/http1.rb +71 -24
- data/lib/raptor/http2.rb +46 -3
- data/lib/raptor/reuseport_bpf.rb +13 -0
- data/lib/raptor/server.rb +14 -6
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +173 -34
- data/sig/generated/raptor/http.rbs +2 -0
- data/sig/generated/raptor/http1.rbs +18 -5
- data/sig/generated/raptor/http2.rbs +16 -0
- data/sig/generated/raptor/reuseport_bpf.rbs +9 -0
- data/sig/generated/raptor/server.rbs +10 -4
- metadata +1 -1
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
|
|
93
|
-
# size would exceed `max_size`, `:malformed`
|
|
94
|
-
#
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
722
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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 { |
|
|
734
|
+
value.each { |entry| header_pairs << [lowered, entry.to_s] }
|
|
692
735
|
else
|
|
693
736
|
header_pairs << [lowered, value.to_s]
|
|
694
737
|
end
|
data/lib/raptor/reuseport_bpf.rb
CHANGED
|
@@ -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
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
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
|
|
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
|
|
data/lib/raptor/version.rb
CHANGED
|
@@ -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
|
-
@
|
|
43
|
+
@bpf_active: bool
|
|
44
44
|
|
|
45
|
-
@
|
|
45
|
+
@server_port: Integer
|
|
46
46
|
|
|
47
|
-
@
|
|
47
|
+
@binder: Binder
|
|
48
48
|
|
|
49
|
-
@
|
|
49
|
+
@on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
50
50
|
|
|
51
|
-
@
|
|
51
|
+
@launch_argv: Array[String]?
|
|
52
52
|
|
|
53
|
-
@
|
|
53
|
+
@launch_command: String?
|
|
54
54
|
|
|
55
|
-
@
|
|
55
|
+
@access_log_io: IO?
|
|
56
56
|
|
|
57
|
-
@
|
|
57
|
+
@access_log_file: String?
|
|
58
58
|
|
|
59
59
|
@stderr_file: String?
|
|
60
60
|
|
|
61
|
-
@
|
|
61
|
+
@stdout_file: String?
|
|
62
62
|
|
|
63
|
-
@
|
|
63
|
+
@pid_file: String?
|
|
64
64
|
|
|
65
|
-
@
|
|
65
|
+
@stats_file: String?
|
|
66
66
|
|
|
67
|
-
@
|
|
67
|
+
@before_refork: Array[Proc]
|
|
68
68
|
|
|
69
|
-
@
|
|
69
|
+
@before_worker_shutdown: Array[Proc]
|
|
70
70
|
|
|
71
|
-
@
|
|
71
|
+
@before_worker_boot: Array[Proc]
|
|
72
72
|
|
|
73
|
-
@
|
|
73
|
+
@before_fork: Array[Proc]
|
|
74
74
|
|
|
75
|
-
@
|
|
75
|
+
@worker_shutdown_timeout: Integer
|
|
76
76
|
|
|
77
|
-
@
|
|
77
|
+
@worker_drain_timeout: Integer
|
|
78
78
|
|
|
79
|
-
@
|
|
79
|
+
@worker_timeout: Integer
|
|
80
80
|
|
|
81
|
-
@
|
|
81
|
+
@worker_boot_timeout: Integer
|
|
82
82
|
|
|
83
|
-
@
|
|
83
|
+
@resp_w: IO?
|
|
84
84
|
|
|
85
|
-
@
|
|
85
|
+
@resp_r: IO?
|
|
86
86
|
|
|
87
|
-
@
|
|
87
|
+
@fork_w: IO?
|
|
88
88
|
|
|
89
|
-
@
|
|
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
|
-
@
|
|
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
|
-
#
|
|
189
|
-
#
|
|
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
|
|
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
|
-
#
|
|
206
|
-
#
|
|
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
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
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.
|