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.
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
- data = begin
198
- socket.read_nonblock(READ_BUFFER_SIZE)
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
- data = begin
534
- socket.read_nonblock(READ_BUFFER_SIZE)
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" : @server_port.to_s
732
+ default_port = behind_tls_proxy ? "443" : @server_port_string
717
733
 
718
734
  http_host = env[Rack::HTTP_HOST]
719
- if http_host
735
+ host = nil
736
+ port = nil
737
+ if http_host && !http_host.empty?
720
738
  if http_host.start_with?("[")
721
- host = http_host[/\A\[([^\]]+)\]/, 1]
722
- port = http_host[/\]:(\d+)\z/, 1]
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
- host, port = http_host.split(":", 2)
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
- # Builds the HTTP status line string.
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
- cache[status].dup
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
- socket_write(socket, response)
1103
+ buffer = response
1077
1104
  while (chunk = file.read(FILE_CHUNK_SIZE))
1078
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
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, optionally buffering it with the headers.
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 << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
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
- socket_write(socket, response)
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
- socket_write(socket, response)
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
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
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
- socket_write(socket, response)
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
- Array(value).flat_map { |entry| entry.to_s.split("\n") }.each do |header_value|
1231
- next if illegal_header_value?(header_value)
1232
-
1233
- result << "#{name}: #{header_value}\r\n"
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 = 64
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 system capacity allows. It checks reactor backlog before accepting
77
- # to prevent overload. This provides natural load balancing across multiple
78
- # worker processes through backpressure control.
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(@binder.listeners, nil, nil, 1)
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 >= [(@thread_pool.size * 1.2).ceil, MIN_BACKPRESSURE_THRESHOLD].max
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
- @binder.close
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
- @binder.listeners.each { |listener| accepted = true if accept_connection(listener) }
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
@@ -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