raptor 0.23.0 → 0.24.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/CHANGELOG.md +16 -0
- data/Dockerfile +7 -1
- data/README.md +183 -68
- data/docs/assets/profile_picture.png +0 -0
- data/docs/assets/startup_output.svg +37 -0
- data/docs/raptor-vs-puma.md +103 -429
- data/docs/talk.md +262 -169
- data/ext/raptor_http/raptor_http.c +82 -123
- data/lib/rackup/handler/raptor.rb +2 -1
- data/lib/raptor/cli.rb +1 -0
- data/lib/raptor/cluster.rb +85 -39
- data/lib/raptor/control_server.rb +2 -0
- data/lib/raptor/http.rb +0 -1
- data/lib/raptor/http1.rb +52 -30
- data/lib/raptor/http2.rb +46 -13
- data/lib/raptor/plugin.rb +98 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +18 -5
- data/sig/generated/raptor/http.rbs +0 -2
- data/sig/generated/raptor/http1.rbs +21 -13
- data/sig/generated/raptor/http2.rbs +12 -9
- data/sig/generated/raptor/plugin.rbs +52 -0
- metadata +5 -1
data/lib/raptor/http1.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Raptor
|
|
|
43
43
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
44
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
45
45
|
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
46
|
+
NOT_IMPLEMENTED_RESPONSE = "HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
46
47
|
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
47
48
|
|
|
48
49
|
CONNECTION_CLOSE = "close"
|
|
@@ -75,31 +76,29 @@ module Raptor
|
|
|
75
76
|
!http_host || http_host.empty? || http_host.include?(",")
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
81
|
-
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
82
|
-
# non-digit character.
|
|
79
|
+
# Rejects ambiguous message framing per RFC 9112 section 6.3 and
|
|
80
|
+
# transfer codings other than a single `chunked`.
|
|
83
81
|
#
|
|
84
82
|
# @param env [Hash] the Rack environment after header parsing
|
|
85
|
-
# @return [
|
|
83
|
+
# @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
|
|
86
84
|
#
|
|
87
|
-
# @rbs (Hash[String, untyped] env) ->
|
|
88
|
-
def self.
|
|
85
|
+
# @rbs (Hash[String, untyped] env) -> Symbol?
|
|
86
|
+
def self.framing_error(env)
|
|
89
87
|
transfer_encoding = env[HTTP_TRANSFER_ENCODING]
|
|
90
88
|
content_length = env[Http::CONTENT_LENGTH]
|
|
91
89
|
|
|
92
90
|
if transfer_encoding
|
|
93
|
-
return
|
|
91
|
+
return :malformed if content_length
|
|
94
92
|
|
|
95
|
-
encodings = transfer_encoding.downcase.split(",").map(&:strip)
|
|
96
|
-
return
|
|
97
|
-
return
|
|
93
|
+
encodings = transfer_encoding.downcase.split(",", -1).map(&:strip)
|
|
94
|
+
return :malformed if encodings.any?(&:empty?) || encodings.last != TRANSFER_ENCODING_CHUNKED
|
|
95
|
+
return :malformed if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
|
|
96
|
+
return :unsupported_encoding if encodings.size > 1
|
|
98
97
|
elsif content_length
|
|
99
|
-
return
|
|
98
|
+
return :malformed unless content_length.match?(/\A[0-9]+\z/)
|
|
100
99
|
end
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
nil
|
|
103
102
|
end
|
|
104
103
|
|
|
105
104
|
# Returns any pipelined bytes following the request, or nil if none remain.
|
|
@@ -144,9 +143,9 @@ module Raptor
|
|
|
144
143
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
145
144
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
146
145
|
# plus populated `:env` and `:body`, and records any bytes after it in
|
|
147
|
-
# the parse data's `:pipelined
|
|
148
|
-
# `:
|
|
149
|
-
# so the reactor can wait for more.
|
|
146
|
+
# the parse data's `:pipelined`. Rejected input sets `:malformed`,
|
|
147
|
+
# `:too_large`, or `:unsupported_encoding`; incomplete input returns
|
|
148
|
+
# the state so the reactor can wait for more.
|
|
150
149
|
#
|
|
151
150
|
# @param data [Hash] the current parse state
|
|
152
151
|
# @param env_template [Hash] the Rack env template to seed the request with
|
|
@@ -170,8 +169,9 @@ module Raptor
|
|
|
170
169
|
parse_data[:parse_count] += 1
|
|
171
170
|
|
|
172
171
|
message = if parser.finished?
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
error = invalid_host?(env) ? :malformed : framing_error(env)
|
|
173
|
+
if error
|
|
174
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, error => true)
|
|
175
175
|
elsif parser.has_body?
|
|
176
176
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
177
177
|
|
|
@@ -325,6 +325,9 @@ module Raptor
|
|
|
325
325
|
when :malformed
|
|
326
326
|
reject_malformed(socket)
|
|
327
327
|
return
|
|
328
|
+
when :unsupported_encoding
|
|
329
|
+
reject_unsupported_encoding(socket)
|
|
330
|
+
return
|
|
328
331
|
end
|
|
329
332
|
|
|
330
333
|
thread_pool << proc do
|
|
@@ -354,6 +357,12 @@ module Raptor
|
|
|
354
357
|
return
|
|
355
358
|
end
|
|
356
359
|
|
|
360
|
+
if parsed_request[:unsupported_encoding]
|
|
361
|
+
socket = reactor.remove(parsed_request[:id])
|
|
362
|
+
reject_unsupported_encoding(socket) if socket
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
357
366
|
if parsed_request[:complete]
|
|
358
367
|
socket = reactor.remove(parsed_request[:id])
|
|
359
368
|
request_count = (parsed_request[:request_count] || 0) + 1
|
|
@@ -444,6 +453,9 @@ module Raptor
|
|
|
444
453
|
when :malformed
|
|
445
454
|
reject_malformed(socket)
|
|
446
455
|
return
|
|
456
|
+
when :unsupported_encoding
|
|
457
|
+
reject_unsupported_encoding(socket)
|
|
458
|
+
return
|
|
447
459
|
end
|
|
448
460
|
|
|
449
461
|
request_count += 1
|
|
@@ -530,11 +542,13 @@ module Raptor
|
|
|
530
542
|
# @param parser [HttpParser] the parser holding the finished parse state
|
|
531
543
|
# @param nread [Integer] the byte offset where the body begins in `buffer`
|
|
532
544
|
# @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
|
|
533
|
-
# `:too_large`, or `:
|
|
545
|
+
# `:too_large`, `:malformed`, or `:unsupported_encoding`
|
|
534
546
|
#
|
|
535
547
|
# @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
|
|
536
548
|
def extract_body(buffer, env, parse_data, parser, nread)
|
|
537
|
-
return :malformed if Http1.invalid_host?(env)
|
|
549
|
+
return :malformed if Http1.invalid_host?(env)
|
|
550
|
+
error = Http1.framing_error(env)
|
|
551
|
+
return error if error
|
|
538
552
|
|
|
539
553
|
unless parser.has_body?
|
|
540
554
|
parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread)
|
|
@@ -809,6 +823,17 @@ module Raptor
|
|
|
809
823
|
socket.close rescue nil
|
|
810
824
|
end
|
|
811
825
|
|
|
826
|
+
# Writes a 501 response and closes the socket.
|
|
827
|
+
#
|
|
828
|
+
# @param socket [TCPSocket] the client socket
|
|
829
|
+
# @return [void]
|
|
830
|
+
#
|
|
831
|
+
# @rbs (TCPSocket socket) -> void
|
|
832
|
+
def reject_unsupported_encoding(socket)
|
|
833
|
+
socket.write(NOT_IMPLEMENTED_RESPONSE) rescue nil
|
|
834
|
+
socket.close rescue nil
|
|
835
|
+
end
|
|
836
|
+
|
|
812
837
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
813
838
|
#
|
|
814
839
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
@@ -855,7 +880,6 @@ module Raptor
|
|
|
855
880
|
end
|
|
856
881
|
|
|
857
882
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
858
|
-
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
859
883
|
|
|
860
884
|
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
861
885
|
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
@@ -890,7 +914,8 @@ module Raptor
|
|
|
890
914
|
|
|
891
915
|
# Builds the `rack.input` IO object for the request body. Returns an
|
|
892
916
|
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
893
|
-
# for larger bodies
|
|
917
|
+
# for larger bodies. The complete body is already buffered at this point,
|
|
918
|
+
# so spooling does not bound peak receive memory.
|
|
894
919
|
#
|
|
895
920
|
# @param body [String, nil] decoded request body
|
|
896
921
|
# @return [IO] an IO-like object positioned at the start of the body
|
|
@@ -934,13 +959,10 @@ module Raptor
|
|
|
934
959
|
def keep_alive?(env, request_count)
|
|
935
960
|
return false if request_count >= @max_keepalive_requests
|
|
936
961
|
|
|
937
|
-
|
|
962
|
+
connection_tokens = env[HTTP_CONNECTION].to_s.downcase.split(",").map(&:strip)
|
|
963
|
+
return false if connection_tokens.include?(CONNECTION_CLOSE)
|
|
938
964
|
|
|
939
|
-
|
|
940
|
-
!connection_header&.casecmp?(CONNECTION_CLOSE)
|
|
941
|
-
else
|
|
942
|
-
connection_header&.casecmp?(CONNECTION_KEEPALIVE) || false
|
|
943
|
-
end
|
|
965
|
+
env[Rack::SERVER_PROTOCOL] == HTTP_11 || connection_tokens.include?(CONNECTION_KEEPALIVE)
|
|
944
966
|
end
|
|
945
967
|
|
|
946
968
|
# Sends an HTTP 103 Early Hints response, skipping any entries with
|
|
@@ -1159,7 +1181,7 @@ module Raptor
|
|
|
1159
1181
|
#
|
|
1160
1182
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1161
1183
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1162
|
-
if body.respond_to?(:call)
|
|
1184
|
+
if body.respond_to?(:call) && !body.respond_to?(:each)
|
|
1163
1185
|
HttpParser.format_headers(response, headers)
|
|
1164
1186
|
response << "\r\n"
|
|
1165
1187
|
socket_write(socket, response)
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -455,7 +455,7 @@ module Raptor
|
|
|
455
455
|
# updated connection state along with any outgoing protocol frames and
|
|
456
456
|
# completed stream requests.
|
|
457
457
|
#
|
|
458
|
-
# @param data [Hash] the connection state including buffer and
|
|
458
|
+
# @param data [Hash] the connection state including buffer, HPACK table and body-size limit
|
|
459
459
|
# @return [Hash] updated state with outgoing_frames and completed_requests
|
|
460
460
|
#
|
|
461
461
|
# @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
|
|
@@ -474,6 +474,7 @@ module Raptor
|
|
|
474
474
|
preface_received = data[:http2_preface_received] || false
|
|
475
475
|
last_client_stream_id = data[:http2_last_client_stream_id] || 0
|
|
476
476
|
pending_headers = data[:http2_pending_headers]
|
|
477
|
+
max_body_size = data[:max_body_size]
|
|
477
478
|
goaway_error = nil
|
|
478
479
|
|
|
479
480
|
unless preface_received
|
|
@@ -586,12 +587,29 @@ module Raptor
|
|
|
586
587
|
stream = streams[stream_id]
|
|
587
588
|
next unless stream
|
|
588
589
|
|
|
590
|
+
if stream[:too_large]
|
|
591
|
+
streams.delete(stream_id) if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
592
|
+
next
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
body = stream[:body] || ""
|
|
596
|
+
if max_body_size && body.bytesize + chunk.bytesize > max_body_size
|
|
597
|
+
headers = parser.encode_headers([[":status", "413"], ["content-length", "0"]])
|
|
598
|
+
outgoing_frames << parser.build_frame(:headers, FLAG_END_HEADERS | FLAG_END_STREAM, stream_id, headers)
|
|
599
|
+
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
600
|
+
streams.delete(stream_id)
|
|
601
|
+
else
|
|
602
|
+
streams[stream_id] = { too_large: true }
|
|
603
|
+
end
|
|
604
|
+
next
|
|
605
|
+
end
|
|
606
|
+
|
|
589
607
|
stream_window_used = (stream[:window_used] || 0) + frame[:length]
|
|
590
608
|
if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
|
|
591
609
|
outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
|
|
592
610
|
stream_window_used = 0
|
|
593
611
|
end
|
|
594
|
-
stream = stream.merge(body:
|
|
612
|
+
stream = stream.merge(body: body + chunk, window_used: stream_window_used)
|
|
595
613
|
|
|
596
614
|
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
597
615
|
stream_headers = stream[:headers] || []
|
|
@@ -714,11 +732,13 @@ module Raptor
|
|
|
714
732
|
outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
|
|
715
733
|
elsif trailers
|
|
716
734
|
stream = streams.delete(stream_id)
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
735
|
+
unless stream[:too_large]
|
|
736
|
+
completed_requests << {
|
|
737
|
+
stream_id: stream_id,
|
|
738
|
+
headers: stream[:headers],
|
|
739
|
+
body: stream[:body] || ""
|
|
740
|
+
}
|
|
741
|
+
end
|
|
722
742
|
else
|
|
723
743
|
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
724
744
|
end
|
|
@@ -783,6 +803,7 @@ module Raptor
|
|
|
783
803
|
id: data[:id],
|
|
784
804
|
protocol: :http2,
|
|
785
805
|
buffer: buffer || "",
|
|
806
|
+
max_body_size: data[:max_body_size],
|
|
786
807
|
hpack_table: hpack_table,
|
|
787
808
|
http2_streams: streams,
|
|
788
809
|
http2_window: connection_window,
|
|
@@ -806,6 +827,7 @@ module Raptor
|
|
|
806
827
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
807
828
|
# @rbs @server_port_string: String
|
|
808
829
|
# @rbs @write_timeout: Integer
|
|
830
|
+
# @rbs @max_body_size: Integer?
|
|
809
831
|
# @rbs @access_log_io: IO?
|
|
810
832
|
# @rbs @clean_thread_locals: bool
|
|
811
833
|
# @rbs @clean_fiber_locals: bool
|
|
@@ -819,6 +841,7 @@ module Raptor
|
|
|
819
841
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
820
842
|
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
821
843
|
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
844
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
822
845
|
# @param http2_options [Hash] HTTP/2-specific settings
|
|
823
846
|
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
824
847
|
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
@@ -832,6 +855,7 @@ module Raptor
|
|
|
832
855
|
@app = app
|
|
833
856
|
@server_port_string = server_port.to_s.freeze
|
|
834
857
|
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
858
|
+
@max_body_size = connection_options[:max_body_size]
|
|
835
859
|
@access_log_io = access_log_io
|
|
836
860
|
@clean_thread_locals = clean_thread_locals
|
|
837
861
|
@clean_fiber_locals = clean_fiber_locals
|
|
@@ -902,6 +926,7 @@ module Raptor
|
|
|
902
926
|
initial_state = {
|
|
903
927
|
id: id,
|
|
904
928
|
protocol: :http2,
|
|
929
|
+
max_body_size: @max_body_size,
|
|
905
930
|
remote_addr: remote_addr,
|
|
906
931
|
url_scheme: url_scheme
|
|
907
932
|
}
|
|
@@ -1140,11 +1165,6 @@ module Raptor
|
|
|
1140
1165
|
trailers: env[RESPONSE_TRAILERS],
|
|
1141
1166
|
request_method: env[Rack::REQUEST_METHOD]
|
|
1142
1167
|
) { response_started = true }
|
|
1143
|
-
if response_body.is_a?(DetachedBody)
|
|
1144
|
-
response_body.finish(:closed)
|
|
1145
|
-
elsif response_body.respond_to?(:close)
|
|
1146
|
-
response_body.close
|
|
1147
|
-
end
|
|
1148
1168
|
write_access_log(env, status, response_size, remote_addr) if @access_log_io
|
|
1149
1169
|
Http.call_response_finished(env, status, response_headers, nil)
|
|
1150
1170
|
rescue StreamClosedError => error
|
|
@@ -1230,6 +1250,12 @@ module Raptor
|
|
|
1230
1250
|
stream.close
|
|
1231
1251
|
|
|
1232
1252
|
stream.bytes_written.to_s
|
|
1253
|
+
ensure
|
|
1254
|
+
if body.is_a?(DetachedBody)
|
|
1255
|
+
body.finish($! ? :cancelled : :closed)
|
|
1256
|
+
elsif body.respond_to?(:close)
|
|
1257
|
+
body.close
|
|
1258
|
+
end
|
|
1233
1259
|
end
|
|
1234
1260
|
|
|
1235
1261
|
# Sends an HTTP 103 Early Hints response.
|
|
@@ -1298,6 +1324,7 @@ module Raptor
|
|
|
1298
1324
|
# @rbs (Array[[String, String]] headers, String body, TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
|
|
1299
1325
|
def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
|
|
1300
1326
|
env = {}
|
|
1327
|
+
underscored_headers = nil
|
|
1301
1328
|
|
|
1302
1329
|
headers.each do |name, value|
|
|
1303
1330
|
if name.start_with?(":")
|
|
@@ -1316,6 +1343,13 @@ module Raptor
|
|
|
1316
1343
|
env[Http::CONTENT_LENGTH] = value
|
|
1317
1344
|
else
|
|
1318
1345
|
rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
1346
|
+
if name.include?("_")
|
|
1347
|
+
next if env.key?(rack_key) && !underscored_headers&.key?(rack_key)
|
|
1348
|
+
|
|
1349
|
+
(underscored_headers ||= {})[rack_key] = true
|
|
1350
|
+
elsif underscored_headers&.delete(rack_key)
|
|
1351
|
+
env.delete(rack_key)
|
|
1352
|
+
end
|
|
1319
1353
|
if (existing = env[rack_key])
|
|
1320
1354
|
separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
|
|
1321
1355
|
env[rack_key] = "#{existing}#{separator}#{value}"
|
|
@@ -1345,7 +1379,6 @@ module Raptor
|
|
|
1345
1379
|
|
|
1346
1380
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
1347
1381
|
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
1348
|
-
env[Http::HTTP_VERSION] = SERVER_PROTOCOL
|
|
1349
1382
|
env[RESPONSE_TRAILERS] = {}
|
|
1350
1383
|
|
|
1351
1384
|
populate_server_name_and_port(env)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Raptor
|
|
5
|
+
# An extension, loaded from `raptor/plugin/<name>` or
|
|
6
|
+
# registered explicitly by application code.
|
|
7
|
+
#
|
|
8
|
+
class Plugin
|
|
9
|
+
class UnknownPlugin < LoadError; end
|
|
10
|
+
|
|
11
|
+
# @rbs self.@registry: Hash[String, singleton(Plugin)]
|
|
12
|
+
@registry = {}
|
|
13
|
+
|
|
14
|
+
# Registers a plugin class under a name.
|
|
15
|
+
#
|
|
16
|
+
# @rbs (String | Symbol name, singleton(Plugin) plugin) -> void
|
|
17
|
+
def self.register(name, plugin)
|
|
18
|
+
@registry[name.to_s] = plugin
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Defines and registers a plugin without a named subclass.
|
|
22
|
+
#
|
|
23
|
+
# @rbs (String | Symbol name) { () -> void } -> singleton(Plugin)
|
|
24
|
+
def self.create(name, &block)
|
|
25
|
+
plugin = Class.new(self, &block)
|
|
26
|
+
register(name, plugin)
|
|
27
|
+
plugin
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Loads a registered plugin, requiring its conventional path if needed.
|
|
31
|
+
#
|
|
32
|
+
# @rbs (String | Symbol name) -> Plugin
|
|
33
|
+
def self.load(name)
|
|
34
|
+
name = name.to_s
|
|
35
|
+
path = "raptor/plugin/#{name}"
|
|
36
|
+
unless @registry.key?(name)
|
|
37
|
+
begin
|
|
38
|
+
require path
|
|
39
|
+
rescue LoadError => error
|
|
40
|
+
raise UnknownPlugin, "Unable to find plugin #{name.inspect}" if error.path == path
|
|
41
|
+
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
plugin = @registry[name]
|
|
47
|
+
raise UnknownPlugin, "#{path} did not register plugin #{name.inspect}" unless plugin
|
|
48
|
+
|
|
49
|
+
plugin.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @rbs @threads: Array[Thread]?
|
|
53
|
+
|
|
54
|
+
# Starts in the master before workers fork. Override to open resources
|
|
55
|
+
# and append callbacks to the cluster's lifecycle hook arrays.
|
|
56
|
+
#
|
|
57
|
+
# @rbs (Cluster cluster) -> void
|
|
58
|
+
def start(cluster)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Stops in the master after workers exit, including before hot restart.
|
|
62
|
+
# Also called if startup fails, so resources may be only partly opened.
|
|
63
|
+
#
|
|
64
|
+
# @rbs () -> void
|
|
65
|
+
def stop
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Runs a block on a master-owned thread. Raptor stops and joins these
|
|
69
|
+
# threads after calling `stop`; use `ensure` in the block for cleanup.
|
|
70
|
+
#
|
|
71
|
+
# @rbs () { () -> void } -> Thread
|
|
72
|
+
def in_background(&block)
|
|
73
|
+
@threads ||= []
|
|
74
|
+
@threads << Thread.new do
|
|
75
|
+
Thread.current.name = "Plugin #{self.class}"
|
|
76
|
+
block.call
|
|
77
|
+
end
|
|
78
|
+
@threads.last
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calls the plugin's stop callback and joins its background threads.
|
|
82
|
+
#
|
|
83
|
+
# @rbs () -> void
|
|
84
|
+
def shutdown
|
|
85
|
+
stop
|
|
86
|
+
ensure
|
|
87
|
+
threads = @threads || []
|
|
88
|
+
threads.each(&:kill)
|
|
89
|
+
error = nil
|
|
90
|
+
threads.each do |thread|
|
|
91
|
+
thread.join
|
|
92
|
+
rescue StandardError => thread_error
|
|
93
|
+
error ||= thread_error
|
|
94
|
+
end
|
|
95
|
+
raise error if error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/raptor/version.rb
CHANGED
|
@@ -66,6 +66,8 @@ module Raptor
|
|
|
66
66
|
|
|
67
67
|
@binder: Binder
|
|
68
68
|
|
|
69
|
+
@plugins: Array[Plugin]
|
|
70
|
+
|
|
69
71
|
@before_refork: Array[Proc]
|
|
70
72
|
|
|
71
73
|
@before_worker_shutdown: Array[Proc]
|
|
@@ -152,6 +154,11 @@ module Raptor
|
|
|
152
154
|
|
|
153
155
|
@drain_accept_queue: bool
|
|
154
156
|
|
|
157
|
+
# Returns the cluster configuration available to plugins.
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash{Symbol => untyped}]
|
|
160
|
+
attr_reader options: Hash[Symbol, untyped]
|
|
161
|
+
|
|
155
162
|
# Creates a new Cluster with the given options.
|
|
156
163
|
#
|
|
157
164
|
# @param options [Hash] cluster configuration options
|
|
@@ -180,6 +187,7 @@ module Raptor
|
|
|
180
187
|
# @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
|
|
181
188
|
# @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
|
|
182
189
|
# @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
|
|
190
|
+
# @option options [Array<String, Symbol>] :plugins plugins to load and start in order
|
|
183
191
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
184
192
|
# @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
|
|
185
193
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
@@ -210,17 +218,22 @@ module Raptor
|
|
|
210
218
|
# @rbs () -> Array[Hash[Symbol, untyped]]
|
|
211
219
|
def stats: () -> Array[Hash[Symbol, untyped]]
|
|
212
220
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
#
|
|
216
|
-
# pool, `max_threads` is the number of threads currently running so
|
|
217
|
-
# utilization reflects the capacity available at that moment.
|
|
221
|
+
# Returns cluster statistics for plugins and the control server.
|
|
222
|
+
# For an adaptive pool, `max_threads` is the number of threads currently
|
|
223
|
+
# running so utilization reflects the capacity available at that moment.
|
|
218
224
|
#
|
|
219
225
|
# @return [Hash] cluster statistics
|
|
220
226
|
#
|
|
221
227
|
# @rbs () -> Hash[Symbol, untyped]
|
|
222
228
|
def stats_hash: () -> Hash[Symbol, untyped]
|
|
223
229
|
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Stops plugins in reverse startup order.
|
|
233
|
+
#
|
|
234
|
+
# @rbs () -> void
|
|
235
|
+
def stop_plugins: () -> void
|
|
236
|
+
|
|
224
237
|
# @rbs (Numeric time) -> String
|
|
225
238
|
def timestamp: (Numeric time) -> String
|
|
226
239
|
|
|
@@ -34,6 +34,8 @@ module Raptor
|
|
|
34
34
|
|
|
35
35
|
INTERNAL_SERVER_ERROR_RESPONSE: ::String
|
|
36
36
|
|
|
37
|
+
NOT_IMPLEMENTED_RESPONSE: ::String
|
|
38
|
+
|
|
37
39
|
CHUNKED_TERMINATOR: ::String
|
|
38
40
|
|
|
39
41
|
CONNECTION_CLOSE: ::String
|
|
@@ -72,17 +74,14 @@ module Raptor
|
|
|
72
74
|
# @rbs (Hash[String, untyped] env) -> bool
|
|
73
75
|
def self.invalid_host?: (Hash[String, untyped] env) -> bool
|
|
74
76
|
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
78
|
-
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
79
|
-
# non-digit character.
|
|
77
|
+
# Rejects ambiguous message framing per RFC 9112 section 6.3 and
|
|
78
|
+
# transfer codings other than a single `chunked`.
|
|
80
79
|
#
|
|
81
80
|
# @param env [Hash] the Rack environment after header parsing
|
|
82
|
-
# @return [
|
|
81
|
+
# @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
|
|
83
82
|
#
|
|
84
|
-
# @rbs (Hash[String, untyped] env) ->
|
|
85
|
-
def self.
|
|
83
|
+
# @rbs (Hash[String, untyped] env) -> Symbol?
|
|
84
|
+
def self.framing_error: (Hash[String, untyped] env) -> Symbol?
|
|
86
85
|
|
|
87
86
|
# Returns any pipelined bytes following the request, or nil if none remain.
|
|
88
87
|
#
|
|
@@ -112,9 +111,9 @@ module Raptor
|
|
|
112
111
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
113
112
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
114
113
|
# plus populated `:env` and `:body`, and records any bytes after it in
|
|
115
|
-
# the parse data's `:pipelined
|
|
116
|
-
# `:
|
|
117
|
-
# so the reactor can wait for more.
|
|
114
|
+
# the parse data's `:pipelined`. Rejected input sets `:malformed`,
|
|
115
|
+
# `:too_large`, or `:unsupported_encoding`; incomplete input returns
|
|
116
|
+
# the state so the reactor can wait for more.
|
|
118
117
|
#
|
|
119
118
|
# @param data [Hash] the current parse state
|
|
120
119
|
# @param env_template [Hash] the Rack env template to seed the request with
|
|
@@ -256,7 +255,7 @@ module Raptor
|
|
|
256
255
|
# @param parser [HttpParser] the parser holding the finished parse state
|
|
257
256
|
# @param nread [Integer] the byte offset where the body begins in `buffer`
|
|
258
257
|
# @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
|
|
259
|
-
# `:too_large`, or `:
|
|
258
|
+
# `:too_large`, `:malformed`, or `:unsupported_encoding`
|
|
260
259
|
#
|
|
261
260
|
# @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
|
|
262
261
|
def extract_body: (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
|
|
@@ -384,6 +383,14 @@ module Raptor
|
|
|
384
383
|
# @rbs (TCPSocket socket) -> void
|
|
385
384
|
def reject_malformed: (TCPSocket socket) -> void
|
|
386
385
|
|
|
386
|
+
# Writes a 501 response and closes the socket.
|
|
387
|
+
#
|
|
388
|
+
# @param socket [TCPSocket] the client socket
|
|
389
|
+
# @return [void]
|
|
390
|
+
#
|
|
391
|
+
# @rbs (TCPSocket socket) -> void
|
|
392
|
+
def reject_unsupported_encoding: (TCPSocket socket) -> void
|
|
393
|
+
|
|
387
394
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
388
395
|
#
|
|
389
396
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
@@ -399,7 +406,8 @@ module Raptor
|
|
|
399
406
|
|
|
400
407
|
# Builds the `rack.input` IO object for the request body. Returns an
|
|
401
408
|
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
402
|
-
# for larger bodies
|
|
409
|
+
# for larger bodies. The complete body is already buffered at this point,
|
|
410
|
+
# so spooling does not bound peak receive memory.
|
|
403
411
|
#
|
|
404
412
|
# @param body [String, nil] decoded request body
|
|
405
413
|
# @return [IO] an IO-like object positioned at the start of the body
|
|
@@ -307,7 +307,7 @@ module Raptor
|
|
|
307
307
|
# updated connection state along with any outgoing protocol frames and
|
|
308
308
|
# completed stream requests.
|
|
309
309
|
#
|
|
310
|
-
# @param data [Hash] the connection state including buffer and
|
|
310
|
+
# @param data [Hash] the connection state including buffer, HPACK table and body-size limit
|
|
311
311
|
# @return [Hash] updated state with outgoing_frames and completed_requests
|
|
312
312
|
#
|
|
313
313
|
# @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
|
|
@@ -381,23 +381,25 @@ module Raptor
|
|
|
381
381
|
# @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[Integer, Integer]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
|
|
382
382
|
def self.build_result: (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[ Integer, Integer ]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
|
|
383
383
|
|
|
384
|
-
@
|
|
384
|
+
@app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
|
|
385
385
|
|
|
386
|
-
@
|
|
386
|
+
@server_port_string: String
|
|
387
387
|
|
|
388
|
-
@
|
|
388
|
+
@write_timeout: Integer
|
|
389
389
|
|
|
390
|
-
@
|
|
390
|
+
@max_body_size: Integer?
|
|
391
|
+
|
|
392
|
+
@access_log_io: IO?
|
|
391
393
|
|
|
392
394
|
@clean_thread_locals: bool
|
|
393
395
|
|
|
394
|
-
@
|
|
396
|
+
@clean_fiber_locals: bool
|
|
395
397
|
|
|
396
|
-
@
|
|
398
|
+
@on_error: (^(Hash[String, untyped]?, Exception) -> void)?
|
|
397
399
|
|
|
398
|
-
@
|
|
400
|
+
@running: AtomicBoolean
|
|
399
401
|
|
|
400
|
-
@
|
|
402
|
+
@initial_settings_frame: String
|
|
401
403
|
|
|
402
404
|
# Creates a new Http2 handler.
|
|
403
405
|
#
|
|
@@ -405,6 +407,7 @@ module Raptor
|
|
|
405
407
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
406
408
|
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
407
409
|
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
410
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
408
411
|
# @param http2_options [Hash] HTTP/2-specific settings
|
|
409
412
|
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
410
413
|
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|