raptor 0.8.0 → 0.9.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/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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.8.0"
5
+ VERSION = "0.9.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
@@ -36,6 +36,19 @@ module Raptor
36
36
  # @rbs (TCPSocket socket, String string, ?timeout: Integer) -> void
37
37
  def self.socket_write: (TCPSocket socket, String string, ?timeout: Integer) -> void
38
38
 
39
+ # Writes `strings` in full via a single `writev(2)` syscall when possible,
40
+ # falling back to per-string writes on partial results. Bounded by
41
+ # `timeout` so a slow client can't pin the writing thread.
42
+ #
43
+ # @param socket [TCPSocket] the socket to write to
44
+ # @param strings [Array<String>] the buffers to write in order
45
+ # @param timeout [Integer] seconds to wait for the socket to become writable on each retry
46
+ # @return [void]
47
+ # @raise [WriteError] if the socket is not writable within the timeout or raises IOError
48
+ #
49
+ # @rbs (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
50
+ def self.socket_writev: (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
51
+
39
52
  # Writes a Common Log Format entry to `io`. Write failures are silently
40
53
  # ignored.
41
54
  #
@@ -14,6 +14,8 @@ module Raptor
14
14
 
15
15
  READ_BUFFER_SIZE: untyped
16
16
 
17
+ RESPONSE_BUFFER_CAPACITY: untyped
18
+
17
19
  KEEPALIVE_READ_TIMEOUT: ::Float
18
20
 
19
21
  MAX_KEEPALIVE_REQUESTS: ::Integer
@@ -132,6 +134,17 @@ module Raptor
132
134
  # @rbs (TCPSocket socket, String string) -> void
133
135
  def socket_write: (TCPSocket socket, String string) -> void
134
136
 
137
+ # Instance-level wrapper around {Http.socket_writev} that applies the
138
+ # configured `write_timeout`.
139
+ #
140
+ # @param socket [TCPSocket] the socket to write to
141
+ # @param strings [Array<String>] the buffers to write in order
142
+ # @return [void]
143
+ # @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
144
+ #
145
+ # @rbs (TCPSocket socket, Array[String] strings) -> void
146
+ def socket_writev: (TCPSocket socket, Array[String] strings) -> void
147
+
135
148
  # Signals eager keep-alive loops to stop processing further requests on
136
149
  # their connections. In-flight requests complete normally.
137
150
  #
@@ -406,7 +419,8 @@ module Raptor
406
419
  # @rbs (Hash[String, String | Array[String]] headers, Integer status) -> void
407
420
  def validate_headers: (Hash[String, String | Array[String]] headers, Integer status) -> void
408
421
 
409
- # Builds the HTTP status line string.
422
+ # Returns the HTTP status line for `status`. Callers must not retain the
423
+ # returned string across further calls on the same thread.
410
424
  #
411
425
  # @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
412
426
  # @param status [Integer] HTTP status code
@@ -499,10 +513,7 @@ module Raptor
499
513
  # @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
500
514
  def write_array_body: (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
501
515
 
502
- # Writes a single-element array body, optionally buffering it with the headers.
503
- #
504
- # Small bodies are concatenated with the headers into one write to reduce
505
- # system call overhead.
516
+ # Writes a single-element array body to the socket.
506
517
  #
507
518
  # @param socket [TCPSocket] the client socket
508
519
  # @param response [String] headers already serialized, to be written before the body
@@ -0,0 +1,56 @@
1
+ # Generated from lib/raptor/reuseport_bpf.rb with RBS::Inline
2
+
3
+ module Raptor
4
+ # Routes incoming connections across worker processes to the worker with
5
+ # the lowest reactor backlog.
6
+ #
7
+ # Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
8
+ # BPF object file has been compiled. Falls back silently to standard
9
+ # `SO_REUSEPORT` when either is missing. Raises when the kernel refuses
10
+ # the BPF program. Wraps `tcp://` bindings only; non-tcp bindings are
11
+ # left to the binder's shared listeners.
12
+ module ReuseportBPF
13
+ LIBBPF_LOADED: untyped
14
+
15
+ BPF_OBJECT_PATH: untyped
16
+
17
+ # Whether the preconditions for BPF-directed reuseport are met on this
18
+ # platform. Does not imply that the kernel will accept the program.
19
+ #
20
+ # @return [Boolean]
21
+ #
22
+ # @rbs () -> bool
23
+ def self.supported?: () -> bool
24
+
25
+ # Prepares BPF-directed routing for `worker_count` workers.
26
+ #
27
+ # Returns true when BPF-directed dispatch is active. Returns false
28
+ # when the platform is unsupported. Raises when the kernel refuses
29
+ # the program.
30
+ #
31
+ # @param worker_count [Integer] number of worker processes that will bind sockets
32
+ # @return [Boolean]
33
+ #
34
+ # @rbs (Integer worker_count) -> bool
35
+ def self.setup: (Integer worker_count) -> bool
36
+
37
+ # Returns tcp listening sockets bound and registered for `worker_index`.
38
+ # Skips non-tcp bind URIs.
39
+ #
40
+ # @param bind_uris [Array<String>] the configured bind URIs
41
+ # @param worker_index [Integer] slot index for this worker in the socks map
42
+ # @param socket_backlog [Integer] kernel listen() queue depth
43
+ # @return [Array<TCPServer>] the tcp listening sockets created for this worker
44
+ #
45
+ # @rbs (Array[String] bind_uris, Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
46
+ def self.create_worker_listeners: (Array[String] bind_uris, Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
47
+
48
+ # Publishes this worker's current backlog to the BPF map.
49
+ #
50
+ # @param backlog [Integer] the worker's current reactor backlog
51
+ # @return [void]
52
+ #
53
+ # @rbs (Integer backlog) -> void
54
+ def self.update_load: (Integer backlog) -> void
55
+ end
56
+ end
@@ -17,7 +17,7 @@ module Raptor
17
17
  # reactor = Reactor.new(ractor_pool, thread_pool, connection_options: {}, http1_options: {})
18
18
  # http1 = Http1.new(app, 3000)
19
19
  # http2 = Http2.new(app, 3000)
20
- # server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 })
20
+ # server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 }, listeners: binder.listeners)
21
21
  # server.run
22
22
  # # ... later
23
23
  # server.shutdown
@@ -36,6 +36,8 @@ module Raptor
36
36
 
37
37
  @running: AtomicBoolean
38
38
 
39
+ @bpf_active: bool
40
+
39
41
  @drain_accept_queue: bool
40
42
 
41
43
  @first_data_timeout: Integer
@@ -48,6 +50,8 @@ module Raptor
48
50
 
49
51
  @reactor: Reactor
50
52
 
53
+ @listeners: Array[TCPServer | UNIXServer | Binder::SslListener]
54
+
51
55
  @binder: Binder
52
56
 
53
57
  # Creates a new Server instance.
@@ -58,18 +62,20 @@ module Raptor
58
62
  # @param http1 [Http1] the HTTP/1.1 handler
59
63
  # @param http2 [Http2] the HTTP/2 handler (provides the initial SETTINGS frame)
60
64
  # @param connection_options [Hash] per-connection timeout configuration, used to bound TLS handshakes
65
+ # @param listeners [Array] the per-worker listeners this server accepts on
61
66
  # @param drain_accept_queue [Boolean] whether to drain the kernel accept queue on shutdown
67
+ # @param worker_index [Integer, nil] the slot index for BPF load reporting
62
68
  # @return [void]
63
69
  #
64
- # @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Http1 http1, Http2 http2, connection_options: Hash[Symbol, untyped], ?drain_accept_queue: bool) -> void
65
- def initialize: (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Http1 http1, Http2 http2, connection_options: Hash[Symbol, untyped], ?drain_accept_queue: bool) -> void
70
+ # @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
71
+ def initialize: (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
66
72
 
67
73
  # Starts the server's main accept loop in a new thread.
68
74
  #
69
75
  # The accept loop polls listening sockets for ready connections and accepts
70
- # them when system capacity allows. It checks reactor backlog before accepting
71
- # to prevent overload. This provides natural load balancing across multiple
72
- # worker processes through backpressure control.
76
+ # them when the reactor backlog is under the backpressure threshold. On
77
+ # Linux with BPF-directed dispatch active, a companion thread publishes
78
+ # this worker's backlog to the BPF map.
73
79
  #
74
80
  # @return [Thread] the thread running the accept loop
75
81
  #
@@ -89,6 +95,14 @@ module Raptor
89
95
 
90
96
  private
91
97
 
98
+ # Starts a background thread that publishes this worker's reactor
99
+ # backlog to the BPF map for load-aware dispatch.
100
+ #
101
+ # @return [void]
102
+ #
103
+ # @rbs () -> void
104
+ def spawn_load_reporter: () -> void
105
+
92
106
  # Dispatches every connection already in the kernel accept queue for each
93
107
  # listener until all are drained.
94
108
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: libbpf-ruby
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
96
110
  email:
97
111
  - djry1999@gmail.com
98
112
  executables:
@@ -100,22 +114,29 @@ executables:
100
114
  extensions:
101
115
  - ext/raptor_http/extconf.rb
102
116
  - ext/raptor_http2/extconf.rb
117
+ - ext/raptor_native/extconf.rb
103
118
  extra_rdoc_files: []
104
119
  files:
105
120
  - ".buildkite/pipeline.yml"
121
+ - ".dockerignore"
106
122
  - ".mise.toml"
107
123
  - Brewfile
108
124
  - CHANGELOG.md
109
125
  - CODE_OF_CONDUCT.md
126
+ - Dockerfile
110
127
  - LICENSE.txt
111
128
  - README.md
112
129
  - Rakefile
130
+ - docs/raptor-vs-puma.md
113
131
  - exe/raptor
132
+ - ext/raptor_bpf/reuseport_select.bpf.c
114
133
  - ext/raptor_http/extconf.rb
115
134
  - ext/raptor_http/raptor_http.c
116
135
  - ext/raptor_http2/extconf.rb
117
136
  - ext/raptor_http2/huffman_table.h
118
137
  - ext/raptor_http2/raptor_http2.c
138
+ - ext/raptor_native/extconf.rb
139
+ - ext/raptor_native/raptor_native.c
119
140
  - lib/rackup/handler/raptor.rb
120
141
  - lib/raptor.rb
121
142
  - lib/raptor/binder.rb
@@ -126,6 +147,7 @@ files:
126
147
  - lib/raptor/http2.rb
127
148
  - lib/raptor/log.rb
128
149
  - lib/raptor/reactor.rb
150
+ - lib/raptor/reuseport_bpf.rb
129
151
  - lib/raptor/server.rb
130
152
  - lib/raptor/stats.rb
131
153
  - lib/raptor/systemd.rb
@@ -140,6 +162,7 @@ files:
140
162
  - sig/generated/raptor/http2.rbs
141
163
  - sig/generated/raptor/log.rbs
142
164
  - sig/generated/raptor/reactor.rbs
165
+ - sig/generated/raptor/reuseport_bpf.rbs
143
166
  - sig/generated/raptor/server.rbs
144
167
  - sig/generated/raptor/stats.rbs
145
168
  - sig/generated/raptor/systemd.rbs