raptor 0.16.0 → 0.17.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 +4 -4
- data/CHANGELOG.md +13 -1
- data/Dockerfile +0 -2
- data/README.md +16 -16
- data/docs/brisrails-talk.md +1 -1
- data/docs/raptor-vs-puma.md +2 -2
- data/ext/raptor_bpf/reuseport_select.bpf.c +19 -28
- data/ext/raptor_http/raptor_http.c +213 -0
- data/ext/raptor_http2/raptor_http2.c +60 -0
- data/lib/rackup/handler/raptor.rb +0 -1
- data/lib/raptor/cluster.rb +6 -6
- data/lib/raptor/http1.rb +37 -158
- data/lib/raptor/http2.rb +1 -16
- data/lib/raptor/reuseport_bpf.rb +20 -3
- data/lib/raptor/server.rb +5 -2
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +6 -4
- data/sig/generated/raptor/http1.rbs +18 -77
- data/sig/generated/raptor/http2.rbs +0 -4
- data/sig/generated/raptor/reuseport_bpf.rbs +12 -2
- data/sig/generated/raptor/server.rbs +3 -2
- metadata +1 -1
data/lib/raptor/http1.rb
CHANGED
|
@@ -21,7 +21,6 @@ module Raptor
|
|
|
21
21
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
22
22
|
CHUNKED_WRITE_THRESHOLD = 512 * 1024
|
|
23
23
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
24
|
-
MAX_CHUNK_OVERHEAD = 16 * 1024
|
|
25
24
|
READ_BUFFER_SIZE = 64 * 1024
|
|
26
25
|
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
27
26
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
@@ -38,7 +37,6 @@ module Raptor
|
|
|
38
37
|
h[status] = "HTTP/1.1 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
STATUS_WITH_NO_ENTITY_BODY = [204, 304, *100..199].freeze
|
|
42
40
|
CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
|
43
41
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
42
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
@@ -48,6 +46,7 @@ module Raptor
|
|
|
48
46
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
49
47
|
EXPECT_100_CONTINUE = "100-continue"
|
|
50
48
|
TRANSFER_ENCODING_CHUNKED = "chunked"
|
|
49
|
+
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
51
50
|
|
|
52
51
|
HTTP_CONNECTION = "HTTP_CONNECTION"
|
|
53
52
|
HTTP_EXPECT = "HTTP_EXPECT"
|
|
@@ -56,10 +55,6 @@ module Raptor
|
|
|
56
55
|
RACK_HIJACKED = "rack.hijacked"
|
|
57
56
|
RACK_HIJACK_IO = "rack.hijack_io"
|
|
58
57
|
|
|
59
|
-
ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
|
|
60
|
-
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
61
|
-
CHUNK_SIZE_REGEX = /\A[0-9A-Fa-f]+\z/
|
|
62
|
-
|
|
63
58
|
# Returns true when an HTTP/1.1 request lacks a valid `Host` header per
|
|
64
59
|
# RFC 9112 section 3.2, where a valid value is a non-empty single-value
|
|
65
60
|
# line.
|
|
@@ -102,59 +97,6 @@ module Raptor
|
|
|
102
97
|
false
|
|
103
98
|
end
|
|
104
99
|
|
|
105
|
-
# Decodes a chunked transfer-encoded body buffer.
|
|
106
|
-
#
|
|
107
|
-
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
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.
|
|
112
|
-
#
|
|
113
|
-
# @param buffer [String] the raw body buffer to decode
|
|
114
|
-
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
115
|
-
# @return [Array(String, Symbol)] decoded body and completion state
|
|
116
|
-
#
|
|
117
|
-
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
118
|
-
def self.decode_chunked(buffer, max_size = nil)
|
|
119
|
-
decoded = String.new
|
|
120
|
-
offset = 0
|
|
121
|
-
overhead = 0
|
|
122
|
-
|
|
123
|
-
while offset < buffer.bytesize
|
|
124
|
-
crlf = buffer.index("\r\n", offset)
|
|
125
|
-
return [decoded, :incomplete] unless crlf
|
|
126
|
-
|
|
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.zero?
|
|
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
|
-
|
|
145
|
-
return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
|
|
146
|
-
|
|
147
|
-
overhead += (crlf - offset) + 4
|
|
148
|
-
return [decoded, :malformed] if overhead > (decoded.bytesize + chunk_size + MAX_CHUNK_OVERHEAD)
|
|
149
|
-
|
|
150
|
-
offset = crlf + 2
|
|
151
|
-
decoded << buffer.byteslice(offset, chunk_size)
|
|
152
|
-
offset += chunk_size + 2
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
[decoded, :incomplete]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
100
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
159
101
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
160
102
|
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
@@ -191,7 +133,7 @@ module Raptor
|
|
|
191
133
|
if max_body_size && parser.content_length > max_body_size
|
|
192
134
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
193
135
|
elsif parser.chunked?
|
|
194
|
-
decoded_body, chunked_state = decode_chunked(body_buffer, max_body_size)
|
|
136
|
+
decoded_body, chunked_state = HttpParser.decode_chunked(body_buffer, max_body_size)
|
|
195
137
|
|
|
196
138
|
case chunked_state
|
|
197
139
|
when :complete
|
|
@@ -479,7 +421,7 @@ module Raptor
|
|
|
479
421
|
if parser.chunked?
|
|
480
422
|
return :incomplete unless decode_chunked
|
|
481
423
|
|
|
482
|
-
body, chunked_state =
|
|
424
|
+
body, chunked_state = HttpParser.decode_chunked(body, @max_body_size)
|
|
483
425
|
case chunked_state
|
|
484
426
|
when :complete
|
|
485
427
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
@@ -581,7 +523,7 @@ module Raptor
|
|
|
581
523
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
582
524
|
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
583
525
|
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
584
|
-
response_size = response_size(headers, body)
|
|
526
|
+
response_size = response_size(headers, body) if @access_log_io && !hijacked
|
|
585
527
|
response_started = true
|
|
586
528
|
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
587
529
|
end
|
|
@@ -919,11 +861,11 @@ module Raptor
|
|
|
919
861
|
|
|
920
862
|
response = +"#{HTTP_11} 103 Early Hints\r\n"
|
|
921
863
|
hints.each do |key, value|
|
|
922
|
-
next if illegal_header_key?(key)
|
|
864
|
+
next if HttpParser.illegal_header_key?(key)
|
|
923
865
|
|
|
924
866
|
values = value.is_a?(Array) ? value : [value]
|
|
925
867
|
values.each do |hint_value|
|
|
926
|
-
next if illegal_header_value?(hint_value.to_s)
|
|
868
|
+
next if HttpParser.illegal_header_value?(hint_value.to_s)
|
|
927
869
|
|
|
928
870
|
response << "#{key.downcase}: #{hint_value}\r\n"
|
|
929
871
|
end
|
|
@@ -948,27 +890,29 @@ module Raptor
|
|
|
948
890
|
validate_status(status)
|
|
949
891
|
response_hijack = headers.is_a?(Hash) ? headers.delete(Rack::RACK_HIJACK) : nil
|
|
950
892
|
headers = normalize_headers(headers)
|
|
951
|
-
|
|
893
|
+
no_entity_body = (status >= 100 && status < 200) || status == 204 || status == 304
|
|
894
|
+
validate_headers(headers, status, no_entity_body)
|
|
952
895
|
|
|
953
896
|
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|
|
954
897
|
|
|
955
898
|
http_version = env[Rack::SERVER_PROTOCOL] == HTTP_11 ? HTTP_11 : HTTP_10
|
|
956
|
-
no_body = env[Rack::REQUEST_METHOD] == "HEAD" ||
|
|
899
|
+
no_body = env[Rack::REQUEST_METHOD] == "HEAD" || no_entity_body
|
|
957
900
|
|
|
958
901
|
response = build_status_line(http_version, status)
|
|
959
902
|
|
|
960
|
-
|
|
903
|
+
corked = !keep_alive
|
|
904
|
+
cork_socket(socket) if corked
|
|
961
905
|
|
|
962
906
|
if response_hijack
|
|
963
907
|
write_hijacked_response(socket, response, headers, response_hijack)
|
|
964
908
|
elsif no_body
|
|
965
|
-
write_no_body_response(socket, response, headers,
|
|
909
|
+
write_no_body_response(socket, response, headers, no_entity_body)
|
|
966
910
|
else
|
|
967
911
|
write_full_response(socket, response, headers, body, http_version)
|
|
968
912
|
end
|
|
969
913
|
ensure
|
|
970
914
|
body.close if body.respond_to?(:close)
|
|
971
|
-
uncork_socket(socket)
|
|
915
|
+
uncork_socket(socket) if corked
|
|
972
916
|
socket.flush rescue nil
|
|
973
917
|
end
|
|
974
918
|
|
|
@@ -1001,7 +945,7 @@ module Raptor
|
|
|
1001
945
|
headers.each do |key, value|
|
|
1002
946
|
raise TypeError, "header keys must be Strings" unless key.is_a?(String)
|
|
1003
947
|
|
|
1004
|
-
next if illegal_header_key?(key)
|
|
948
|
+
next if HttpParser.illegal_header_key?(key)
|
|
1005
949
|
|
|
1006
950
|
normalized_key = key.match?(/[A-Z]/) ? key.downcase : key
|
|
1007
951
|
next if normalized_key.start_with?(RACK_HEADER_PREFIX)
|
|
@@ -1017,12 +961,13 @@ module Raptor
|
|
|
1017
961
|
#
|
|
1018
962
|
# @param headers [Hash] normalized response headers
|
|
1019
963
|
# @param status [Integer] HTTP status code
|
|
964
|
+
# @param no_entity_body [Boolean] whether the status forbids an entity body
|
|
1020
965
|
# @return [void]
|
|
1021
966
|
# @raise [ArgumentError] if a forbidden header is present for the status
|
|
1022
967
|
#
|
|
1023
|
-
# @rbs (Hash[String, String | Array[String]] headers, Integer status) -> void
|
|
1024
|
-
def validate_headers(headers, status)
|
|
1025
|
-
if
|
|
968
|
+
# @rbs (Hash[String, String | Array[String]] headers, Integer status, bool no_entity_body) -> void
|
|
969
|
+
def validate_headers(headers, status, no_entity_body)
|
|
970
|
+
if no_entity_body
|
|
1026
971
|
raise ArgumentError, "content-type must not be present for status #{status}" if headers.key?(Rack::CONTENT_TYPE)
|
|
1027
972
|
|
|
1028
973
|
raise ArgumentError, "content-length must not be present for status #{status}" if headers.key?(Rack::CONTENT_LENGTH)
|
|
@@ -1055,7 +1000,7 @@ module Raptor
|
|
|
1055
1000
|
#
|
|
1056
1001
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, ^(TCPSocket) -> void response_hijack) -> void
|
|
1057
1002
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
1058
|
-
format_headers(response, headers)
|
|
1003
|
+
HttpParser.format_headers(response, headers)
|
|
1059
1004
|
response << "\r\n"
|
|
1060
1005
|
socket_write(socket, response)
|
|
1061
1006
|
uncork_socket(socket)
|
|
@@ -1069,16 +1014,16 @@ module Raptor
|
|
|
1069
1014
|
# @param socket [TCPSocket] the client socket
|
|
1070
1015
|
# @param response [String] the status line accumulated so far
|
|
1071
1016
|
# @param headers [Hash] normalized response headers
|
|
1072
|
-
# @param
|
|
1017
|
+
# @param no_entity_body [Boolean] whether the response status forbids an entity body
|
|
1073
1018
|
# @return [void]
|
|
1074
1019
|
#
|
|
1075
|
-
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers,
|
|
1076
|
-
def write_no_body_response(socket, response, headers,
|
|
1077
|
-
unless
|
|
1020
|
+
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, bool no_entity_body) -> void
|
|
1021
|
+
def write_no_body_response(socket, response, headers, no_entity_body)
|
|
1022
|
+
unless no_entity_body
|
|
1078
1023
|
headers[Rack::CONTENT_LENGTH] = "0" unless headers.key?(Rack::CONTENT_LENGTH) || headers.key?(Rack::TRANSFER_ENCODING)
|
|
1079
1024
|
end
|
|
1080
1025
|
|
|
1081
|
-
format_headers(response, headers)
|
|
1026
|
+
HttpParser.format_headers(response, headers)
|
|
1082
1027
|
response << "\r\n"
|
|
1083
1028
|
socket_write(socket, response)
|
|
1084
1029
|
end
|
|
@@ -1097,7 +1042,7 @@ module Raptor
|
|
|
1097
1042
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1098
1043
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1099
1044
|
if body.respond_to?(:call)
|
|
1100
|
-
format_headers(response, headers)
|
|
1045
|
+
HttpParser.format_headers(response, headers)
|
|
1101
1046
|
response << "\r\n"
|
|
1102
1047
|
socket_write(socket, response)
|
|
1103
1048
|
uncork_socket(socket)
|
|
@@ -1123,7 +1068,7 @@ module Raptor
|
|
|
1123
1068
|
headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED
|
|
1124
1069
|
end
|
|
1125
1070
|
|
|
1126
|
-
format_headers(response, headers)
|
|
1071
|
+
HttpParser.format_headers(response, headers)
|
|
1127
1072
|
response << "\r\n"
|
|
1128
1073
|
|
|
1129
1074
|
if body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
|
@@ -1172,13 +1117,13 @@ module Raptor
|
|
|
1172
1117
|
if use_chunked
|
|
1173
1118
|
buffer = response
|
|
1174
1119
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
1175
|
-
buffer
|
|
1120
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1176
1121
|
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1177
1122
|
socket_write(socket, buffer)
|
|
1178
1123
|
buffer = +""
|
|
1179
1124
|
end
|
|
1180
1125
|
end
|
|
1181
|
-
buffer <<
|
|
1126
|
+
buffer << CHUNKED_TERMINATOR
|
|
1182
1127
|
socket_write(socket, buffer)
|
|
1183
1128
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
1184
1129
|
response << file.read(content_length)
|
|
@@ -1221,7 +1166,8 @@ module Raptor
|
|
|
1221
1166
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1222
1167
|
|
|
1223
1168
|
if use_chunked
|
|
1224
|
-
response
|
|
1169
|
+
HttpParser.chunked_encode(response, chunk)
|
|
1170
|
+
response << CHUNKED_TERMINATOR
|
|
1225
1171
|
socket_write(socket, response)
|
|
1226
1172
|
else
|
|
1227
1173
|
socket_writev(socket, [response, chunk])
|
|
@@ -1244,15 +1190,13 @@ module Raptor
|
|
|
1244
1190
|
body_array.each do |chunk|
|
|
1245
1191
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1246
1192
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
|
|
1193
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1250
1194
|
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1251
1195
|
socket_write(socket, buffer)
|
|
1252
1196
|
buffer = +""
|
|
1253
1197
|
end
|
|
1254
1198
|
end
|
|
1255
|
-
buffer <<
|
|
1199
|
+
buffer << CHUNKED_TERMINATOR
|
|
1256
1200
|
socket_write(socket, buffer)
|
|
1257
1201
|
else
|
|
1258
1202
|
body_array.each do |chunk|
|
|
@@ -1275,14 +1219,15 @@ module Raptor
|
|
|
1275
1219
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
1276
1220
|
if use_chunked
|
|
1277
1221
|
socket_write(socket, response)
|
|
1222
|
+
buffer = +""
|
|
1278
1223
|
body.each do |chunk|
|
|
1279
1224
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1280
1225
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
socket_write(socket,
|
|
1226
|
+
buffer.clear
|
|
1227
|
+
HttpParser.chunked_encode(buffer, chunk)
|
|
1228
|
+
socket_write(socket, buffer) unless buffer.empty?
|
|
1284
1229
|
end
|
|
1285
|
-
socket_write(socket,
|
|
1230
|
+
socket_write(socket, CHUNKED_TERMINATOR)
|
|
1286
1231
|
else
|
|
1287
1232
|
body.each do |chunk|
|
|
1288
1233
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
@@ -1293,72 +1238,6 @@ module Raptor
|
|
|
1293
1238
|
end
|
|
1294
1239
|
end
|
|
1295
1240
|
|
|
1296
|
-
# Returns true if the header key contains characters illegal in HTTP headers.
|
|
1297
|
-
#
|
|
1298
|
-
# @param key [String] the header key to check
|
|
1299
|
-
# @return [Boolean] true if the key is illegal
|
|
1300
|
-
#
|
|
1301
|
-
# @rbs (String key) -> bool
|
|
1302
|
-
def illegal_header_key?(key)
|
|
1303
|
-
key.match?(ILLEGAL_HEADER_KEY_REGEX)
|
|
1304
|
-
end
|
|
1305
|
-
|
|
1306
|
-
# Returns true if the header value contains characters illegal in HTTP headers.
|
|
1307
|
-
#
|
|
1308
|
-
# @param value [String] the header value to check
|
|
1309
|
-
# @return [Boolean] true if the value is illegal
|
|
1310
|
-
#
|
|
1311
|
-
# @rbs (String value) -> bool
|
|
1312
|
-
def illegal_header_value?(value)
|
|
1313
|
-
value.match?(ILLEGAL_HEADER_VALUE_REGEX)
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
|
-
# Appends normalised header lines to `result`. Skips entries with
|
|
1317
|
-
# illegal keys or values. Array values are written as separate lines.
|
|
1318
|
-
#
|
|
1319
|
-
# @param headers [Hash] normalized response headers
|
|
1320
|
-
# @return [String] formatted header lines, each ending with CRLF
|
|
1321
|
-
#
|
|
1322
|
-
# @rbs (String result, Hash[String, String | Array[String]] headers) -> void
|
|
1323
|
-
def format_headers(result, headers)
|
|
1324
|
-
headers.each do |name, value|
|
|
1325
|
-
next if illegal_header_key?(name)
|
|
1326
|
-
|
|
1327
|
-
if value.is_a?(Array)
|
|
1328
|
-
value.each { |entry| append_header_value(result, name, entry) }
|
|
1329
|
-
else
|
|
1330
|
-
append_header_value(result, name, value)
|
|
1331
|
-
end
|
|
1332
|
-
end
|
|
1333
|
-
end
|
|
1334
|
-
|
|
1335
|
-
# Appends one or more `name: value` header lines to `result`, splitting
|
|
1336
|
-
# newline-joined values across separate lines and skipping empty or
|
|
1337
|
-
# illegal values.
|
|
1338
|
-
#
|
|
1339
|
-
# @param result [String] the buffer to append to
|
|
1340
|
-
# @param name [String] the header name
|
|
1341
|
-
# @param value [Object] the header value (any object responding to `to_s`)
|
|
1342
|
-
# @return [void]
|
|
1343
|
-
#
|
|
1344
|
-
# @rbs (String result, String name, untyped value) -> void
|
|
1345
|
-
def append_header_value(result, name, value)
|
|
1346
|
-
string_value = value.is_a?(String) ? value : value.to_s
|
|
1347
|
-
return if string_value.empty?
|
|
1348
|
-
|
|
1349
|
-
if string_value.include?("\n")
|
|
1350
|
-
string_value.split("\n").each do |line|
|
|
1351
|
-
next if line.empty? || illegal_header_value?(line)
|
|
1352
|
-
|
|
1353
|
-
result << name << ": " << line << "\r\n"
|
|
1354
|
-
end
|
|
1355
|
-
else
|
|
1356
|
-
return if illegal_header_value?(string_value)
|
|
1357
|
-
|
|
1358
|
-
result << name << ": " << string_value << "\r\n"
|
|
1359
|
-
end
|
|
1360
|
-
end
|
|
1361
|
-
|
|
1362
1241
|
# Calls every `rack.response_finished` callback in reverse
|
|
1363
1242
|
# registration order, rescuing any that raise.
|
|
1364
1243
|
#
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -229,8 +229,6 @@ module Raptor
|
|
|
229
229
|
MAX_FRAME_SIZE = 16_384
|
|
230
230
|
|
|
231
231
|
SERVER_PROTOCOL = "HTTP/2"
|
|
232
|
-
RACK_HEADER_PREFIX = "rack."
|
|
233
|
-
HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
|
|
234
232
|
|
|
235
233
|
REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path", ":authority"].freeze
|
|
236
234
|
REQUIRED_REQUEST_PSEUDO_HEADERS = [":method", ":scheme", ":path"].freeze
|
|
@@ -766,20 +764,7 @@ module Raptor
|
|
|
766
764
|
def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
|
|
767
765
|
parser = Http2Parser.new
|
|
768
766
|
|
|
769
|
-
|
|
770
|
-
headers.each do |name, value|
|
|
771
|
-
lowered = name.downcase
|
|
772
|
-
next if lowered.start_with?(RACK_HEADER_PREFIX)
|
|
773
|
-
next if HOP_BY_HOP_HEADERS.include?(lowered)
|
|
774
|
-
|
|
775
|
-
if value.is_a?(Array)
|
|
776
|
-
value.each { |entry| header_pairs << [lowered, entry.to_s] }
|
|
777
|
-
else
|
|
778
|
-
header_pairs << [lowered, value.to_s]
|
|
779
|
-
end
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
encoded_headers = parser.encode_headers(header_pairs)
|
|
767
|
+
encoded_headers = parser.encode_response_headers(status, headers)
|
|
783
768
|
body_chunks = []
|
|
784
769
|
body_bytes = 0
|
|
785
770
|
body.each do |chunk|
|
data/lib/raptor/reuseport_bpf.rb
CHANGED
|
@@ -10,8 +10,8 @@ rescue LoadError
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
module Raptor
|
|
13
|
-
# Routes incoming connections across worker processes
|
|
14
|
-
# the
|
|
13
|
+
# Routes incoming connections across worker processes using two-choice
|
|
14
|
+
# load sampling, atomically reserving the selected slot before routing.
|
|
15
15
|
#
|
|
16
16
|
# Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
|
|
17
17
|
# BPF object file has been compiled. Falls back silently to standard
|
|
@@ -47,7 +47,7 @@ module Raptor
|
|
|
47
47
|
return false unless supported?
|
|
48
48
|
|
|
49
49
|
@object = LibBPFRuby::Object.new(BPF_OBJECT_PATH)
|
|
50
|
-
@program_fd = @object.program_fd("
|
|
50
|
+
@program_fd = @object.program_fd("select_less_loaded")
|
|
51
51
|
@socks_fd = @object.map_fd("socks")
|
|
52
52
|
@loads_fd = @object.map_fd("loads")
|
|
53
53
|
|
|
@@ -99,6 +99,7 @@ module Raptor
|
|
|
99
99
|
def self.enable_load_reporting(worker_index)
|
|
100
100
|
@load_key = [worker_index + 1].pack("L")
|
|
101
101
|
@load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
102
|
+
@accepted_load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
# Publishes this worker's current backlog to the BPF map.
|
|
@@ -115,6 +116,22 @@ module Raptor
|
|
|
115
116
|
LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
|
|
116
117
|
end
|
|
117
118
|
|
|
119
|
+
# Publishes an accept-side backlog estimate. This uses a separate value
|
|
120
|
+
# buffer because the server and load reporter call into the map from
|
|
121
|
+
# different threads.
|
|
122
|
+
#
|
|
123
|
+
# @param backlog [Integer] the estimated backlog after accepting a socket
|
|
124
|
+
# @return [void]
|
|
125
|
+
#
|
|
126
|
+
# @rbs (Integer backlog) -> void
|
|
127
|
+
def self.update_accepted_load(backlog)
|
|
128
|
+
@accepted_load_value.setbyte(0, backlog & 0xff)
|
|
129
|
+
@accepted_load_value.setbyte(1, (backlog >> 8) & 0xff)
|
|
130
|
+
@accepted_load_value.setbyte(2, (backlog >> 16) & 0xff)
|
|
131
|
+
@accepted_load_value.setbyte(3, (backlog >> 24) & 0xff)
|
|
132
|
+
LibBPFRuby.map_update(@loads_fd, @load_key, @accepted_load_value)
|
|
133
|
+
end
|
|
134
|
+
|
|
118
135
|
# Marks a worker's slot as unavailable by parking its load at the
|
|
119
136
|
# sentinel maximum so the BPF program's least-loaded pick skips it.
|
|
120
137
|
#
|
data/lib/raptor/server.rb
CHANGED
|
@@ -152,8 +152,9 @@ module Raptor
|
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
# Accepts a connection from the given listener
|
|
156
|
-
# the
|
|
155
|
+
# Accepts a connection from the given listener, immediately publishes
|
|
156
|
+
# the pending work to the BPF map, and dispatches the socket into the
|
|
157
|
+
# HTTP/1.1 or SSL/HTTP-2 pipeline.
|
|
157
158
|
#
|
|
158
159
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
159
160
|
# @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
|
|
@@ -172,6 +173,8 @@ module Raptor
|
|
|
172
173
|
remote_addr = DEFAULT_REMOTE_ADDR
|
|
173
174
|
end
|
|
174
175
|
|
|
176
|
+
ReuseportBPF.update_accepted_load(@reactor.backlog + 1) if @bpf_active
|
|
177
|
+
|
|
175
178
|
if listener.is_a?(Binder::SslListener)
|
|
176
179
|
@thread_pool << proc do
|
|
177
180
|
dispatch_ssl_connection(listener, tcp_client, remote_addr)
|
data/lib/raptor/version.rb
CHANGED
|
@@ -26,10 +26,11 @@ module Raptor
|
|
|
26
26
|
# `[1, HTTP1_RACTOR_COUNT_CAP]`.
|
|
27
27
|
#
|
|
28
28
|
# @param worker_count [Integer] the configured worker count
|
|
29
|
+
# @param cores [Integer] the available processor count
|
|
29
30
|
# @return [Integer]
|
|
30
31
|
#
|
|
31
|
-
# @rbs (Integer worker_count) -> Integer
|
|
32
|
-
def self.default_http1_ractor_count: (Integer worker_count) -> Integer
|
|
32
|
+
# @rbs (Integer worker_count, ?cores: Integer) -> Integer
|
|
33
|
+
def self.default_http1_ractor_count: (Integer worker_count, ?cores: Integer) -> Integer
|
|
33
34
|
|
|
34
35
|
# Returns the default number of HTTP/2 pipeline ractors per
|
|
35
36
|
# worker for the given worker count, rounded from the available
|
|
@@ -37,10 +38,11 @@ module Raptor
|
|
|
37
38
|
# `[1, HTTP2_RACTOR_COUNT_CAP]`.
|
|
38
39
|
#
|
|
39
40
|
# @param worker_count [Integer] the configured worker count
|
|
41
|
+
# @param cores [Integer] the available processor count
|
|
40
42
|
# @return [Integer]
|
|
41
43
|
#
|
|
42
|
-
# @rbs (Integer worker_count) -> Integer
|
|
43
|
-
def self.default_http2_ractor_count: (Integer worker_count) -> Integer
|
|
44
|
+
# @rbs (Integer worker_count, ?cores: Integer) -> Integer
|
|
45
|
+
def self.default_http2_ractor_count: (Integer worker_count, ?cores: Integer) -> Integer
|
|
44
46
|
|
|
45
47
|
@server_port: Integer
|
|
46
48
|
|