raptor 0.8.0 → 0.10.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 +90 -32
- data/.dockerignore +5 -0
- data/CHANGELOG.md +22 -0
- data/Dockerfile +28 -0
- data/README.md +17 -9
- data/Rakefile +19 -1
- data/docs/raptor-vs-puma.md +557 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +63 -0
- data/ext/raptor_http/extconf.rb +1 -1
- data/ext/raptor_http/raptor_http.c +87 -6
- data/ext/raptor_native/extconf.rb +7 -0
- data/ext/raptor_native/raptor_native.c +99 -0
- data/lib/raptor/binder.rb +19 -4
- data/lib/raptor/cli.rb +1 -1
- data/lib/raptor/cluster.rb +27 -2
- data/lib/raptor/http.rb +41 -0
- data/lib/raptor/http1.rb +115 -56
- data/lib/raptor/reuseport_bpf.rb +109 -0
- data/lib/raptor/server.rb +40 -12
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +10 -0
- data/sig/generated/raptor/cluster.rbs +4 -2
- data/sig/generated/raptor/http.rbs +13 -0
- data/sig/generated/raptor/http1.rbs +42 -13
- data/sig/generated/raptor/reuseport_bpf.rbs +56 -0
- data/sig/generated/raptor/server.rbs +20 -6
- metadata +24 -1
data/lib/raptor/http1.rb
CHANGED
|
@@ -19,9 +19,11 @@ module Raptor
|
|
|
19
19
|
#
|
|
20
20
|
class Http1
|
|
21
21
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
22
|
+
CHUNKED_WRITE_THRESHOLD = 512 * 1024
|
|
22
23
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
23
24
|
MAX_CHUNK_OVERHEAD = 16 * 1024
|
|
24
25
|
READ_BUFFER_SIZE = 64 * 1024
|
|
26
|
+
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
25
27
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
26
28
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
27
29
|
|
|
@@ -122,6 +124,7 @@ module Raptor
|
|
|
122
124
|
|
|
123
125
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
124
126
|
# @rbs @server_port: Integer
|
|
127
|
+
# @rbs @server_port_string: String
|
|
125
128
|
# @rbs @write_timeout: Integer
|
|
126
129
|
# @rbs @max_body_size: Integer?
|
|
127
130
|
# @rbs @body_spool_threshold: Integer?
|
|
@@ -129,6 +132,7 @@ module Raptor
|
|
|
129
132
|
# @rbs @access_log_io: IO?
|
|
130
133
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
131
134
|
# @rbs @running: AtomicBoolean
|
|
135
|
+
# @rbs @env_template: Hash[String, untyped]
|
|
132
136
|
|
|
133
137
|
# Creates a new Http1 handler.
|
|
134
138
|
#
|
|
@@ -148,6 +152,7 @@ module Raptor
|
|
|
148
152
|
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
|
|
149
153
|
@app = app
|
|
150
154
|
@server_port = server_port
|
|
155
|
+
@server_port_string = server_port.to_s.freeze
|
|
151
156
|
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
152
157
|
@max_body_size = connection_options[:max_body_size]
|
|
153
158
|
@body_spool_threshold = connection_options[:body_spool_threshold]
|
|
@@ -155,6 +160,13 @@ module Raptor
|
|
|
155
160
|
@access_log_io = access_log_io
|
|
156
161
|
@on_error = on_error
|
|
157
162
|
@running = AtomicBoolean.new(true)
|
|
163
|
+
@env_template = {
|
|
164
|
+
Rack::RACK_VERSION => Rack::VERSION,
|
|
165
|
+
Rack::RACK_IS_HIJACK => true,
|
|
166
|
+
Rack::SCRIPT_NAME => "",
|
|
167
|
+
Rack::QUERY_STRING => "",
|
|
168
|
+
Http::SERVER_SOFTWARE => Http::SERVER_SOFTWARE_VALUE
|
|
169
|
+
}.freeze
|
|
158
170
|
end
|
|
159
171
|
|
|
160
172
|
# Instance-level wrapper around {Http.socket_write} that applies the
|
|
@@ -170,6 +182,19 @@ module Raptor
|
|
|
170
182
|
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
171
183
|
end
|
|
172
184
|
|
|
185
|
+
# Instance-level wrapper around {Http.socket_writev} that applies the
|
|
186
|
+
# configured `write_timeout`.
|
|
187
|
+
#
|
|
188
|
+
# @param socket [TCPSocket] the socket to write to
|
|
189
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
190
|
+
# @return [void]
|
|
191
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
192
|
+
#
|
|
193
|
+
# @rbs (TCPSocket socket, Array[String] strings) -> void
|
|
194
|
+
def socket_writev(socket, strings)
|
|
195
|
+
Http.socket_writev(socket, strings, timeout: @write_timeout)
|
|
196
|
+
end
|
|
197
|
+
|
|
173
198
|
# Signals eager keep-alive loops to stop processing further requests on
|
|
174
199
|
# their connections. In-flight requests complete normally.
|
|
175
200
|
#
|
|
@@ -194,8 +219,10 @@ module Raptor
|
|
|
194
219
|
#
|
|
195
220
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
196
221
|
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
223
|
+
|
|
224
|
+
begin
|
|
225
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
199
226
|
rescue IO::WaitReadable
|
|
200
227
|
reactor.add(
|
|
201
228
|
id: id,
|
|
@@ -209,15 +236,12 @@ module Raptor
|
|
|
209
236
|
return
|
|
210
237
|
end
|
|
211
238
|
|
|
212
|
-
buffer = String.new
|
|
213
|
-
buffer << data
|
|
214
|
-
|
|
215
239
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
216
240
|
buffer << socket.read_nonblock(socket.pending)
|
|
217
241
|
end
|
|
218
242
|
|
|
219
243
|
parser = HttpParser.new
|
|
220
|
-
env =
|
|
244
|
+
env = @env_template.dup
|
|
221
245
|
nread = begin
|
|
222
246
|
parser.execute(env, buffer, 0)
|
|
223
247
|
rescue HttpParserError
|
|
@@ -278,12 +302,13 @@ module Raptor
|
|
|
278
302
|
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
279
303
|
def http_parser_worker
|
|
280
304
|
max_body_size = @max_body_size
|
|
305
|
+
env_template = @env_template
|
|
281
306
|
|
|
282
307
|
proc do |data|
|
|
283
308
|
next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
|
|
284
309
|
|
|
285
310
|
parser = Raptor::HttpParser.new
|
|
286
|
-
env =
|
|
311
|
+
env = env_template.dup
|
|
287
312
|
nread = begin
|
|
288
313
|
parser.execute(env, data[:buffer], 0)
|
|
289
314
|
rescue Raptor::HttpParserError
|
|
@@ -530,8 +555,10 @@ module Raptor
|
|
|
530
555
|
return
|
|
531
556
|
end
|
|
532
557
|
|
|
533
|
-
|
|
534
|
-
|
|
558
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
559
|
+
|
|
560
|
+
begin
|
|
561
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
535
562
|
rescue IO::WaitReadable
|
|
536
563
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
537
564
|
return
|
|
@@ -540,15 +567,12 @@ module Raptor
|
|
|
540
567
|
return
|
|
541
568
|
end
|
|
542
569
|
|
|
543
|
-
buffer = String.new
|
|
544
|
-
buffer << data
|
|
545
|
-
|
|
546
570
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
547
571
|
buffer << socket.read_nonblock(socket.pending)
|
|
548
572
|
end
|
|
549
573
|
|
|
550
574
|
parser = HttpParser.new
|
|
551
|
-
env =
|
|
575
|
+
env = @env_template.dup
|
|
552
576
|
nread = begin
|
|
553
577
|
parser.execute(env, buffer, 0)
|
|
554
578
|
rescue HttpParserError
|
|
@@ -631,7 +655,7 @@ module Raptor
|
|
|
631
655
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
632
656
|
state = {
|
|
633
657
|
id: id,
|
|
634
|
-
buffer: buffer,
|
|
658
|
+
buffer: buffer.dup,
|
|
635
659
|
env: env,
|
|
636
660
|
request_count: request_count,
|
|
637
661
|
parse_data: parse_data,
|
|
@@ -682,53 +706,54 @@ module Raptor
|
|
|
682
706
|
#
|
|
683
707
|
# @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
|
|
684
708
|
def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
|
|
685
|
-
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
686
709
|
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
687
710
|
env[Rack::RACK_ERRORS] = $stderr
|
|
688
711
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
689
|
-
|
|
690
|
-
env[Rack::RACK_IS_HIJACK] = true
|
|
691
712
|
env[Rack::RACK_HIJACK] = proc do
|
|
692
713
|
env[RACK_HIJACKED] = true
|
|
693
714
|
env[RACK_HIJACK_IO] = socket
|
|
694
715
|
socket
|
|
695
716
|
end
|
|
696
|
-
|
|
697
717
|
env[Rack::RACK_EARLY_HINTS] = proc do |hints|
|
|
698
718
|
send_early_hints(socket, hints) rescue nil
|
|
699
719
|
end
|
|
700
720
|
|
|
701
|
-
env[Rack::SCRIPT_NAME] = "" unless env.key?(Rack::SCRIPT_NAME)
|
|
702
721
|
env[Rack::PATH_INFO] = env.delete(Rack::REQUEST_PATH) if env.key?(Rack::REQUEST_PATH)
|
|
703
722
|
env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
|
|
704
|
-
env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
|
|
705
|
-
|
|
706
723
|
if (content_length = parse_data[:content_length]).positive?
|
|
707
724
|
env[Http::CONTENT_LENGTH] = content_length.to_s
|
|
708
725
|
end
|
|
709
726
|
|
|
710
727
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
711
|
-
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
712
728
|
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
713
729
|
|
|
714
730
|
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
715
731
|
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
716
|
-
default_port = behind_tls_proxy ? "443" : @
|
|
732
|
+
default_port = behind_tls_proxy ? "443" : @server_port_string
|
|
717
733
|
|
|
718
734
|
http_host = env[Rack::HTTP_HOST]
|
|
719
|
-
|
|
735
|
+
host = nil
|
|
736
|
+
port = nil
|
|
737
|
+
if http_host && !http_host.empty?
|
|
720
738
|
if http_host.start_with?("[")
|
|
721
|
-
|
|
722
|
-
|
|
739
|
+
bracket_end = http_host.index("]")
|
|
740
|
+
if bracket_end
|
|
741
|
+
host = http_host.byteslice(1, bracket_end - 1)
|
|
742
|
+
port_colon = http_host.index(":", bracket_end + 1)
|
|
743
|
+
port = port_colon && http_host.byteslice(port_colon + 1, http_host.bytesize - port_colon - 1)
|
|
744
|
+
end
|
|
723
745
|
else
|
|
724
|
-
|
|
746
|
+
colon = http_host.index(":")
|
|
747
|
+
if colon
|
|
748
|
+
host = http_host.byteslice(0, colon)
|
|
749
|
+
port = http_host.byteslice(colon + 1, http_host.bytesize - colon - 1)
|
|
750
|
+
else
|
|
751
|
+
host = http_host
|
|
752
|
+
end
|
|
725
753
|
end
|
|
726
|
-
env[Rack::SERVER_NAME] ||= host
|
|
727
|
-
env[Rack::SERVER_PORT] ||= port || default_port
|
|
728
|
-
else
|
|
729
|
-
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
730
|
-
env[Rack::SERVER_PORT] ||= default_port
|
|
731
754
|
end
|
|
755
|
+
env[Rack::SERVER_NAME] ||= host || Server::DEFAULT_SERVER_NAME
|
|
756
|
+
env[Rack::SERVER_PORT] ||= port || default_port
|
|
732
757
|
|
|
733
758
|
env
|
|
734
759
|
end
|
|
@@ -922,7 +947,8 @@ module Raptor
|
|
|
922
947
|
end
|
|
923
948
|
end
|
|
924
949
|
|
|
925
|
-
#
|
|
950
|
+
# Returns the HTTP status line for `status`. Callers must not retain the
|
|
951
|
+
# returned string across further calls on the same thread.
|
|
926
952
|
#
|
|
927
953
|
# @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
|
|
928
954
|
# @param status [Integer] HTTP status code
|
|
@@ -931,7 +957,10 @@ module Raptor
|
|
|
931
957
|
# @rbs (String http_version, Integer status) -> String
|
|
932
958
|
def build_status_line(http_version, status)
|
|
933
959
|
cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
|
|
934
|
-
|
|
960
|
+
response = (Thread.current[:raptor_response_buffer] ||= String.new(capacity: RESPONSE_BUFFER_CAPACITY))
|
|
961
|
+
response.clear
|
|
962
|
+
response << cache[status]
|
|
963
|
+
response
|
|
935
964
|
end
|
|
936
965
|
|
|
937
966
|
# Writes response headers and delegates body writing to the hijack callback.
|
|
@@ -1032,8 +1061,6 @@ module Raptor
|
|
|
1032
1061
|
else
|
|
1033
1062
|
raise TypeError, "body must respond to each, to_ary, or to_path"
|
|
1034
1063
|
end
|
|
1035
|
-
|
|
1036
|
-
socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
1037
1064
|
end
|
|
1038
1065
|
|
|
1039
1066
|
# Calculates content length from an array or file body without consuming it.
|
|
@@ -1073,10 +1100,16 @@ module Raptor
|
|
|
1073
1100
|
def write_file_body(socket, response, path, content_length, use_chunked)
|
|
1074
1101
|
File.open(path, "rb") do |file|
|
|
1075
1102
|
if use_chunked
|
|
1076
|
-
|
|
1103
|
+
buffer = response
|
|
1077
1104
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
1078
|
-
|
|
1105
|
+
buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
|
|
1106
|
+
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1107
|
+
socket_write(socket, buffer)
|
|
1108
|
+
buffer = +""
|
|
1109
|
+
end
|
|
1079
1110
|
end
|
|
1111
|
+
buffer << "0\r\n\r\n"
|
|
1112
|
+
socket_write(socket, buffer)
|
|
1080
1113
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
1081
1114
|
response << file.read(content_length)
|
|
1082
1115
|
socket_write(socket, response)
|
|
@@ -1106,10 +1139,7 @@ module Raptor
|
|
|
1106
1139
|
end
|
|
1107
1140
|
end
|
|
1108
1141
|
|
|
1109
|
-
# Writes a single-element array body
|
|
1110
|
-
#
|
|
1111
|
-
# Small bodies are concatenated with the headers into one write to reduce
|
|
1112
|
-
# system call overhead.
|
|
1142
|
+
# Writes a single-element array body to the socket.
|
|
1113
1143
|
#
|
|
1114
1144
|
# @param socket [TCPSocket] the client socket
|
|
1115
1145
|
# @param response [String] headers already serialized, to be written before the body
|
|
@@ -1123,13 +1153,10 @@ module Raptor
|
|
|
1123
1153
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1124
1154
|
|
|
1125
1155
|
if use_chunked
|
|
1126
|
-
response <<
|
|
1156
|
+
response << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n0\r\n\r\n"
|
|
1127
1157
|
socket_write(socket, response)
|
|
1128
|
-
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
1129
|
-
socket_write(socket, response << chunk)
|
|
1130
1158
|
else
|
|
1131
|
-
|
|
1132
|
-
socket_write(socket, chunk)
|
|
1159
|
+
socket_writev(socket, [response, chunk])
|
|
1133
1160
|
end
|
|
1134
1161
|
end
|
|
1135
1162
|
|
|
@@ -1145,21 +1172,25 @@ module Raptor
|
|
|
1145
1172
|
# @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
|
|
1146
1173
|
def write_multiple_chunks(socket, response, body_array, use_chunked)
|
|
1147
1174
|
if use_chunked
|
|
1148
|
-
|
|
1175
|
+
buffer = response
|
|
1149
1176
|
body_array.each do |chunk|
|
|
1150
1177
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1151
1178
|
|
|
1152
1179
|
next if chunk.empty?
|
|
1153
1180
|
|
|
1154
|
-
|
|
1181
|
+
buffer << chunk.bytesize.to_s(16) << "\r\n" << chunk << "\r\n"
|
|
1182
|
+
if buffer.bytesize >= CHUNKED_WRITE_THRESHOLD
|
|
1183
|
+
socket_write(socket, buffer)
|
|
1184
|
+
buffer = +""
|
|
1185
|
+
end
|
|
1155
1186
|
end
|
|
1187
|
+
buffer << "0\r\n\r\n"
|
|
1188
|
+
socket_write(socket, buffer)
|
|
1156
1189
|
else
|
|
1157
1190
|
body_array.each do |chunk|
|
|
1158
1191
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1159
|
-
|
|
1160
|
-
response << chunk
|
|
1161
1192
|
end
|
|
1162
|
-
|
|
1193
|
+
socket_writev(socket, [response, *body_array])
|
|
1163
1194
|
end
|
|
1164
1195
|
end
|
|
1165
1196
|
|
|
@@ -1183,6 +1214,7 @@ module Raptor
|
|
|
1183
1214
|
|
|
1184
1215
|
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1185
1216
|
end
|
|
1217
|
+
socket_write(socket, "0\r\n\r\n")
|
|
1186
1218
|
else
|
|
1187
1219
|
body.each do |chunk|
|
|
1188
1220
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
@@ -1227,15 +1259,42 @@ module Raptor
|
|
|
1227
1259
|
headers.each do |name, value|
|
|
1228
1260
|
next if illegal_header_key?(name)
|
|
1229
1261
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
result
|
|
1262
|
+
if value.is_a?(Array)
|
|
1263
|
+
value.each { |entry| append_header_value(result, name, entry) }
|
|
1264
|
+
else
|
|
1265
|
+
append_header_value(result, name, value)
|
|
1234
1266
|
end
|
|
1235
1267
|
end
|
|
1236
1268
|
result
|
|
1237
1269
|
end
|
|
1238
1270
|
|
|
1271
|
+
# Appends one or more `name: value` header lines to `result`. Newline-
|
|
1272
|
+
# separated values are emitted as separate lines; empty values and values
|
|
1273
|
+
# with illegal characters are skipped silently.
|
|
1274
|
+
#
|
|
1275
|
+
# @param result [String] the buffer to append to
|
|
1276
|
+
# @param name [String] the header name
|
|
1277
|
+
# @param value [Object] the header value (any object responding to `to_s`)
|
|
1278
|
+
# @return [void]
|
|
1279
|
+
#
|
|
1280
|
+
# @rbs (String result, String name, untyped value) -> void
|
|
1281
|
+
def append_header_value(result, name, value)
|
|
1282
|
+
string_value = value.is_a?(String) ? value : value.to_s
|
|
1283
|
+
return if string_value.empty?
|
|
1284
|
+
|
|
1285
|
+
if string_value.include?("\n")
|
|
1286
|
+
string_value.split("\n").each do |line|
|
|
1287
|
+
next if line.empty? || illegal_header_value?(line)
|
|
1288
|
+
|
|
1289
|
+
result << name << ": " << line << "\r\n"
|
|
1290
|
+
end
|
|
1291
|
+
else
|
|
1292
|
+
return if illegal_header_value?(string_value)
|
|
1293
|
+
|
|
1294
|
+
result << name << ": " << string_value << "\r\n"
|
|
1295
|
+
end
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1239
1298
|
# Calls all rack.response_finished callbacks registered in the environment.
|
|
1240
1299
|
#
|
|
1241
1300
|
# Callbacks are called in reverse registration order. Individual callback
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "socket"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require "libbpf-ruby"
|
|
9
|
+
rescue LoadError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Raptor
|
|
13
|
+
# Routes incoming connections across worker processes to the worker with
|
|
14
|
+
# the lowest reactor backlog.
|
|
15
|
+
#
|
|
16
|
+
# Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
|
|
17
|
+
# BPF object file has been compiled. Falls back silently to standard
|
|
18
|
+
# `SO_REUSEPORT` when either is missing. Raises when the kernel refuses
|
|
19
|
+
# the BPF program. Wraps `tcp://` bindings only; non-tcp bindings are
|
|
20
|
+
# left to the binder's shared listeners.
|
|
21
|
+
#
|
|
22
|
+
module ReuseportBPF
|
|
23
|
+
LIBBPF_LOADED = !!defined?(LibBPFRuby)
|
|
24
|
+
BPF_OBJECT_PATH = File.expand_path("../../ext/raptor_bpf/reuseport_select.bpf.o", __dir__)
|
|
25
|
+
|
|
26
|
+
# Whether the preconditions for BPF-directed reuseport are met on this
|
|
27
|
+
# platform. Does not imply that the kernel will accept the program.
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
#
|
|
31
|
+
# @rbs () -> bool
|
|
32
|
+
def self.supported?
|
|
33
|
+
LIBBPF_LOADED && File.exist?(BPF_OBJECT_PATH)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Prepares BPF-directed routing for `worker_count` workers.
|
|
37
|
+
#
|
|
38
|
+
# Returns true when BPF-directed dispatch is active. Returns false
|
|
39
|
+
# when the platform is unsupported. Raises when the kernel refuses
|
|
40
|
+
# the program.
|
|
41
|
+
#
|
|
42
|
+
# @param worker_count [Integer] number of worker processes that will bind sockets
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
#
|
|
45
|
+
# @rbs (Integer worker_count) -> bool
|
|
46
|
+
def self.setup(worker_count)
|
|
47
|
+
return false unless supported?
|
|
48
|
+
|
|
49
|
+
@object = LibBPFRuby::Object.new(BPF_OBJECT_PATH)
|
|
50
|
+
@program_fd = @object.program_fd("select_least_loaded")
|
|
51
|
+
@socks_fd = @object.map_fd("socks")
|
|
52
|
+
@loads_fd = @object.map_fd("loads")
|
|
53
|
+
|
|
54
|
+
LibBPFRuby.map_update(@loads_fd, [0].pack("L"), [worker_count].pack("L"))
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns tcp listening sockets bound and registered for `worker_index`.
|
|
59
|
+
# Skips non-tcp bind URIs.
|
|
60
|
+
#
|
|
61
|
+
# @param bind_uris [Array<String>] the configured bind URIs
|
|
62
|
+
# @param worker_index [Integer] slot index for this worker in the socks map
|
|
63
|
+
# @param socket_backlog [Integer] kernel listen() queue depth
|
|
64
|
+
# @return [Array<TCPServer>] the tcp listening sockets created for this worker
|
|
65
|
+
#
|
|
66
|
+
# @rbs (Array[String] bind_uris, Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
|
|
67
|
+
def self.create_worker_listeners(bind_uris, worker_index, socket_backlog)
|
|
68
|
+
@load_key = [worker_index + 1].pack("L")
|
|
69
|
+
@load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
70
|
+
|
|
71
|
+
bind_uris.filter_map do |bind_uri|
|
|
72
|
+
uri = URI(bind_uri)
|
|
73
|
+
next unless uri.scheme == "tcp"
|
|
74
|
+
|
|
75
|
+
host = uri.host
|
|
76
|
+
host = host[1..-2] if host&.start_with?("[")
|
|
77
|
+
|
|
78
|
+
addrinfo = Addrinfo.tcp(host, uri.port)
|
|
79
|
+
socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
|
|
80
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
81
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
|
82
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
83
|
+
socket.bind(addrinfo)
|
|
84
|
+
socket.listen(socket_backlog)
|
|
85
|
+
|
|
86
|
+
LibBPFRuby.sockmap_update(@socks_fd, [worker_index].pack("L"), socket)
|
|
87
|
+
LibBPFRuby.attach_reuseport(socket, @program_fd)
|
|
88
|
+
|
|
89
|
+
tcp_server = TCPServer.for_fd(socket.fileno)
|
|
90
|
+
socket.autoclose = false
|
|
91
|
+
tcp_server
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Publishes this worker's current backlog to the BPF map.
|
|
96
|
+
#
|
|
97
|
+
# @param backlog [Integer] the worker's current reactor backlog
|
|
98
|
+
# @return [void]
|
|
99
|
+
#
|
|
100
|
+
# @rbs (Integer backlog) -> void
|
|
101
|
+
def self.update_load(backlog)
|
|
102
|
+
@load_value.setbyte(0, backlog & 0xff)
|
|
103
|
+
@load_value.setbyte(1, (backlog >> 8) & 0xff)
|
|
104
|
+
@load_value.setbyte(2, (backlog >> 16) & 0xff)
|
|
105
|
+
@load_value.setbyte(3, (backlog >> 24) & 0xff)
|
|
106
|
+
LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/raptor/server.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "socket"
|
|
|
5
5
|
|
|
6
6
|
require "atomic-ruby/atomic_boolean"
|
|
7
7
|
|
|
8
|
+
require_relative "reuseport_bpf"
|
|
9
|
+
|
|
8
10
|
module Raptor
|
|
9
11
|
# Accepts client connections and dispatches them into the request
|
|
10
12
|
# pipeline. Skips acceptance when the reactor backlog is high so an
|
|
@@ -22,7 +24,7 @@ module Raptor
|
|
|
22
24
|
# reactor = Reactor.new(ractor_pool, thread_pool, connection_options: {}, http1_options: {})
|
|
23
25
|
# http1 = Http1.new(app, 3000)
|
|
24
26
|
# http2 = Http2.new(app, 3000)
|
|
25
|
-
# server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 })
|
|
27
|
+
# server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 }, listeners: binder.listeners)
|
|
26
28
|
# server.run
|
|
27
29
|
# # ... later
|
|
28
30
|
# server.shutdown
|
|
@@ -36,15 +38,17 @@ module Raptor
|
|
|
36
38
|
DEFAULT_REMOTE_ADDR = "127.0.0.1"
|
|
37
39
|
DEFAULT_SERVER_NAME = "localhost"
|
|
38
40
|
|
|
39
|
-
MIN_BACKPRESSURE_THRESHOLD =
|
|
41
|
+
MIN_BACKPRESSURE_THRESHOLD = 8
|
|
40
42
|
|
|
41
43
|
# @rbs @binder: Binder
|
|
44
|
+
# @rbs @listeners: Array[TCPServer | UNIXServer | Binder::SslListener]
|
|
42
45
|
# @rbs @reactor: Reactor
|
|
43
46
|
# @rbs @thread_pool: AtomicThreadPool
|
|
44
47
|
# @rbs @http1: Http1
|
|
45
48
|
# @rbs @http2: Http2
|
|
46
49
|
# @rbs @first_data_timeout: Integer
|
|
47
50
|
# @rbs @drain_accept_queue: bool
|
|
51
|
+
# @rbs @bpf_active: bool
|
|
48
52
|
# @rbs @running: AtomicBoolean
|
|
49
53
|
|
|
50
54
|
# Creates a new Server instance.
|
|
@@ -55,44 +59,52 @@ module Raptor
|
|
|
55
59
|
# @param http1 [Http1] the HTTP/1.1 handler
|
|
56
60
|
# @param http2 [Http2] the HTTP/2 handler (provides the initial SETTINGS frame)
|
|
57
61
|
# @param connection_options [Hash] per-connection timeout configuration, used to bound TLS handshakes
|
|
62
|
+
# @param listeners [Array] the per-worker listeners this server accepts on
|
|
58
63
|
# @param drain_accept_queue [Boolean] whether to drain the kernel accept queue on shutdown
|
|
64
|
+
# @param worker_index [Integer, nil] the slot index for BPF load reporting
|
|
59
65
|
# @return [void]
|
|
60
66
|
#
|
|
61
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Http1 http1, Http2 http2, connection_options: Hash[Symbol, untyped], ?drain_accept_queue: bool) -> void
|
|
62
|
-
def initialize(binder, reactor, thread_pool, http1, http2, connection_options:, drain_accept_queue: false)
|
|
67
|
+
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Http1 http1, Http2 http2, connection_options: Hash[Symbol, untyped], listeners: Array[untyped], ?drain_accept_queue: bool, ?worker_index: Integer?) -> void
|
|
68
|
+
def initialize(binder, reactor, thread_pool, http1, http2, connection_options:, listeners:, drain_accept_queue: false, worker_index: nil)
|
|
63
69
|
@binder = binder
|
|
70
|
+
@listeners = listeners
|
|
64
71
|
@reactor = reactor
|
|
65
72
|
@thread_pool = thread_pool
|
|
66
73
|
@http1 = http1
|
|
67
74
|
@http2 = http2
|
|
68
75
|
@first_data_timeout = connection_options[:first_data_timeout]
|
|
69
76
|
@drain_accept_queue = drain_accept_queue
|
|
77
|
+
@bpf_active = !!worker_index
|
|
70
78
|
@running = AtomicBoolean.new(true)
|
|
71
79
|
end
|
|
72
80
|
|
|
73
81
|
# Starts the server's main accept loop in a new thread.
|
|
74
82
|
#
|
|
75
83
|
# The accept loop polls listening sockets for ready connections and accepts
|
|
76
|
-
# them when
|
|
77
|
-
#
|
|
78
|
-
# worker
|
|
84
|
+
# them when the reactor backlog is under the backpressure threshold. On
|
|
85
|
+
# Linux with BPF-directed dispatch active, a companion thread publishes
|
|
86
|
+
# this worker's backlog to the BPF map.
|
|
79
87
|
#
|
|
80
88
|
# @return [Thread] the thread running the accept loop
|
|
81
89
|
#
|
|
82
90
|
# @rbs () -> Thread
|
|
83
91
|
def run
|
|
92
|
+
spawn_load_reporter if @bpf_active
|
|
93
|
+
|
|
84
94
|
Thread.new do
|
|
85
95
|
Thread.current.name = "Server"
|
|
86
96
|
|
|
97
|
+
backpressure_threshold = [(@thread_pool.size * 1.2).ceil, MIN_BACKPRESSURE_THRESHOLD].max
|
|
98
|
+
|
|
87
99
|
while @running.true?
|
|
88
100
|
begin
|
|
89
|
-
ready_servers, _, _ = IO.select(@
|
|
101
|
+
ready_servers, _, _ = IO.select(@listeners, nil, nil, 1)
|
|
90
102
|
rescue IOError, Errno::EBADF
|
|
91
103
|
break
|
|
92
104
|
end
|
|
93
105
|
|
|
94
106
|
next unless ready_servers
|
|
95
|
-
next if @reactor.backlog >=
|
|
107
|
+
next if @reactor.backlog >= backpressure_threshold
|
|
96
108
|
|
|
97
109
|
ready_servers.each { |listener| accept_connection(listener) }
|
|
98
110
|
end
|
|
@@ -111,11 +123,28 @@ module Raptor
|
|
|
111
123
|
def shutdown
|
|
112
124
|
@running.make_false
|
|
113
125
|
drain_accept_queue if @drain_accept_queue
|
|
114
|
-
@
|
|
126
|
+
@listeners.each(&:close)
|
|
115
127
|
end
|
|
116
128
|
|
|
117
129
|
private
|
|
118
130
|
|
|
131
|
+
# Starts a background thread that publishes this worker's reactor
|
|
132
|
+
# backlog to the BPF map for load-aware dispatch.
|
|
133
|
+
#
|
|
134
|
+
# @return [void]
|
|
135
|
+
#
|
|
136
|
+
# @rbs () -> void
|
|
137
|
+
def spawn_load_reporter
|
|
138
|
+
Thread.new do
|
|
139
|
+
Thread.current.name = "Load Reporter"
|
|
140
|
+
|
|
141
|
+
while @running.true?
|
|
142
|
+
ReuseportBPF.update_load(@reactor.backlog)
|
|
143
|
+
sleep 0.01
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
119
148
|
# Dispatches every connection already in the kernel accept queue for each
|
|
120
149
|
# listener until all are drained.
|
|
121
150
|
#
|
|
@@ -125,7 +154,7 @@ module Raptor
|
|
|
125
154
|
def drain_accept_queue
|
|
126
155
|
loop do
|
|
127
156
|
accepted = false
|
|
128
|
-
@
|
|
157
|
+
@listeners.each { |listener| accepted = true if accept_connection(listener) }
|
|
129
158
|
break unless accepted
|
|
130
159
|
end
|
|
131
160
|
end
|
|
@@ -150,7 +179,6 @@ module Raptor
|
|
|
150
179
|
end
|
|
151
180
|
|
|
152
181
|
if tcp_client.is_a?(TCPSocket)
|
|
153
|
-
tcp_client.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
154
182
|
remote_addr = tcp_client.remote_address.ip_address
|
|
155
183
|
else
|
|
156
184
|
remote_addr = DEFAULT_REMOTE_ADDR
|
data/lib/raptor/version.rb
CHANGED
|
@@ -61,6 +61,16 @@ module Raptor
|
|
|
61
61
|
|
|
62
62
|
@bind_uris: Array[String]
|
|
63
63
|
|
|
64
|
+
# Array of bind URIs.
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<String>] the bind URIs
|
|
67
|
+
attr_reader bind_uris: untyped
|
|
68
|
+
|
|
69
|
+
# Kernel listen() queue depth for TCP/SSL listeners.
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer] the socket backlog
|
|
72
|
+
attr_reader socket_backlog: untyped
|
|
73
|
+
|
|
64
74
|
# Array of listening sockets.
|
|
65
75
|
#
|
|
66
76
|
# @return [Array<TCPServer, UNIXServer, SslListener>] the server sockets
|
|
@@ -40,8 +40,6 @@ module Raptor
|
|
|
40
40
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
41
41
|
def self.run: (Hash[Symbol, untyped] options) -> void
|
|
42
42
|
|
|
43
|
-
@http1_options: Hash[Symbol, untyped]
|
|
44
|
-
|
|
45
43
|
@http2_options: Hash[Symbol, untyped]
|
|
46
44
|
|
|
47
45
|
@worker_boot_timeout: Integer
|
|
@@ -92,6 +90,10 @@ module Raptor
|
|
|
92
90
|
|
|
93
91
|
@hot_restart_requested: bool
|
|
94
92
|
|
|
93
|
+
@bpf_active: bool
|
|
94
|
+
|
|
95
|
+
@http1_options: Hash[Symbol, untyped]
|
|
96
|
+
|
|
95
97
|
@connection_options: Hash[Symbol, untyped]
|
|
96
98
|
|
|
97
99
|
@environment: String
|