raptor 0.22.2 → 0.23.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/Dockerfile +3 -1
  4. data/README.md +38 -31
  5. data/docs/raptor-vs-puma.md +44 -38
  6. data/docs/talk.md +1295 -0
  7. data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
  8. data/ext/raptor_http/extconf.rb +2 -0
  9. data/ext/raptor_http/raptor_http.c +91 -119
  10. data/ext/raptor_http2/raptor_http2.c +272 -223
  11. data/ext/raptor_native/raptor_native.c +38 -20
  12. data/lib/rackup/handler/raptor.rb +12 -11
  13. data/lib/raptor/binder.rb +12 -12
  14. data/lib/raptor/cli.rb +18 -18
  15. data/lib/raptor/cluster.rb +44 -44
  16. data/lib/raptor/control_server.rb +3 -2
  17. data/lib/raptor/detached_body.rb +6 -2
  18. data/lib/raptor/http.rb +7 -5
  19. data/lib/raptor/http1.rb +233 -208
  20. data/lib/raptor/http2.rb +314 -267
  21. data/lib/raptor/reactor.rb +139 -149
  22. data/lib/raptor/reuseport_bpf.rb +62 -38
  23. data/lib/raptor/server.rb +53 -38
  24. data/lib/raptor/stats.rb +15 -3
  25. data/lib/raptor/thread_locals.rb +4 -3
  26. data/lib/raptor/version.rb +1 -1
  27. data/sig/generated/rackup/handler/raptor.rbs +2 -1
  28. data/sig/generated/raptor/binder.rbs +9 -9
  29. data/sig/generated/raptor/cli.rbs +6 -6
  30. data/sig/generated/raptor/cluster.rbs +9 -9
  31. data/sig/generated/raptor/control_server.rbs +2 -1
  32. data/sig/generated/raptor/detached_body.rbs +5 -1
  33. data/sig/generated/raptor/http.rbs +3 -3
  34. data/sig/generated/raptor/http1.rbs +100 -91
  35. data/sig/generated/raptor/http2.rbs +200 -189
  36. data/sig/generated/raptor/reactor.rbs +107 -118
  37. data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
  38. data/sig/generated/raptor/server.rbs +26 -26
  39. data/sig/generated/raptor/stats.rbs +3 -2
  40. data/sig/generated/raptor/thread_locals.rbs +4 -3
  41. metadata +2 -2
  42. data/docs/brisrails-talk.md +0 -2398
@@ -2,7 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "socket"
5
- require "uri"
6
5
 
7
6
  begin
8
7
  require "libbpf-ruby"
@@ -15,13 +14,28 @@ module Raptor
15
14
  #
16
15
  # Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
17
16
  # 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.
17
+ # `SO_REUSEPORT` when either is missing, or when there are more workers
18
+ # or listeners than the BPF maps have slots for. Raises when the kernel
19
+ # refuses the BPF program. Wraps `tcp://` bindings only; non-tcp bindings
20
+ # are left to the binder's shared listeners.
21
21
  #
22
22
  module ReuseportBPF
23
23
  LIBBPF_LOADED = !!defined?(LibBPFRuby)
24
24
  BPF_OBJECT_PATH = File.expand_path("../../ext/raptor_bpf/reuseport_select.bpf.o", __dir__)
25
+ TCP_INFO_UNACKED_OFFSET = 24
26
+ UNAVAILABLE_LOAD = 0xffffffff
27
+ MAX_WORKERS = 64
28
+ MAX_LISTENERS = 16
29
+
30
+ # @rbs self.@object: untyped
31
+ # @rbs self.@program_fd: Integer
32
+ # @rbs self.@socks_fd: Integer
33
+ # @rbs self.@loads_fd: Integer?
34
+ # @rbs self.@shared_listeners: Array[TCPServer]
35
+ # @rbs self.@worker_listeners: Array[TCPServer]
36
+ # @rbs self.@load_key: String
37
+ # @rbs self.@load_value: String
38
+ # @rbs self.@accepted_load_value: String
25
39
 
26
40
  # Returns true when the preconditions for BPF-directed reuseport are
27
41
  # met on this platform.
@@ -33,18 +47,22 @@ module Raptor
33
47
  LIBBPF_LOADED && File.exist?(BPF_OBJECT_PATH)
34
48
  end
35
49
 
36
- # Prepares BPF-directed routing for `worker_count` workers.
50
+ # Prepares BPF-directed routing for `worker_count` workers across the
51
+ # addresses `shared_listeners` are bound to.
37
52
  #
38
53
  # Returns true when BPF-directed dispatch is active. Returns false
39
- # when the platform is unsupported. Raises when the kernel refuses
40
- # the program.
54
+ # when the platform is unsupported or the counts exceed the map slots.
55
+ # Raises when the kernel refuses the program.
41
56
  #
42
57
  # @param worker_count [Integer] number of worker processes that will bind sockets
58
+ # @param shared_listeners [Array<TCPServer>] the binder's tcp listeners
43
59
  # @return [Boolean]
44
60
  #
45
- # @rbs (Integer worker_count) -> bool
46
- def self.setup(worker_count)
47
- return false unless supported?
61
+ # @rbs (Integer worker_count, Array[TCPServer] shared_listeners) -> bool
62
+ def self.setup(worker_count, shared_listeners)
63
+ return false unless supported? && worker_count <= MAX_WORKERS && shared_listeners.length <= MAX_LISTENERS
64
+
65
+ @shared_listeners = shared_listeners
48
66
 
49
67
  @object = LibBPFRuby::Object.new(BPF_OBJECT_PATH)
50
68
  @program_fd = @object.program_fd("select_less_loaded")
@@ -55,32 +73,27 @@ module Raptor
55
73
  true
56
74
  end
57
75
 
58
- # Returns tcp listening sockets bound and registered for `worker_index`.
59
- # Skips non-tcp bind URIs.
76
+ # Creates this worker's BPF-routed listeners at the shared listeners'
77
+ # bound addresses, including resolved `localhost` addresses and ports
78
+ # assigned by the kernel for port 0.
60
79
  #
61
- # @param bind_uris [Array<String>] the configured bind URIs
62
80
  # @param worker_index [Integer] slot index for this worker in the socks map
63
81
  # @param socket_backlog [Integer] kernel listen() queue depth
64
82
  # @return [Array<TCPServer>] the tcp listening sockets created for this worker
65
83
  #
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
- bind_uris.filter_map do |bind_uri|
69
- uri = URI(bind_uri)
70
- next unless uri.scheme == "tcp"
71
-
72
- host = uri.host
73
- host = host[1..-2] if host&.start_with?("[")
74
-
75
- addrinfo = Addrinfo.tcp(host, uri.port)
84
+ # @rbs (Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
85
+ def self.create_worker_listeners(worker_index, socket_backlog)
86
+ @worker_listeners = @shared_listeners.each_with_index.map do |shared_listener, listener_index|
87
+ addrinfo = shared_listener.local_address
76
88
  socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
77
89
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
78
90
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
79
91
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
92
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_DEFER_ACCEPT, 1)
80
93
  socket.bind(addrinfo)
81
94
  socket.listen(socket_backlog)
82
95
 
83
- LibBPFRuby.sockmap_update(@socks_fd, [worker_index].pack("L"), socket)
96
+ LibBPFRuby.sockmap_update(@socks_fd, [listener_index * MAX_WORKERS + worker_index].pack("L"), socket)
84
97
  LibBPFRuby.attach_reuseport(socket, @program_fd)
85
98
 
86
99
  tcp_server = TCPServer.for_fd(socket.fileno)
@@ -102,21 +115,22 @@ module Raptor
102
115
  @accepted_load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
103
116
  end
104
117
 
105
- # Publishes this worker's current backlog to the BPF map.
118
+ # Publishes this worker's load to the BPF map.
106
119
  #
107
120
  # @param backlog [Integer] the worker's current reactor backlog
108
121
  # @return [void]
109
122
  #
110
123
  # @rbs (Integer backlog) -> void
111
124
  def self.update_load(backlog)
112
- @load_value.setbyte(0, backlog & 0xff)
113
- @load_value.setbyte(1, (backlog >> 8) & 0xff)
114
- @load_value.setbyte(2, (backlog >> 16) & 0xff)
115
- @load_value.setbyte(3, (backlog >> 24) & 0xff)
125
+ load = backlog + accept_queue_length
126
+ @load_value.setbyte(0, load & 0xff)
127
+ @load_value.setbyte(1, (load >> 8) & 0xff)
128
+ @load_value.setbyte(2, (load >> 16) & 0xff)
129
+ @load_value.setbyte(3, (load >> 24) & 0xff)
116
130
  LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
117
131
  end
118
132
 
119
- # Publishes an accept-side backlog estimate. This uses a separate value
133
+ # Publishes an accept-side load estimate. This uses a separate value
120
134
  # buffer because the server and load reporter call into the map from
121
135
  # different threads.
122
136
  #
@@ -125,24 +139,34 @@ module Raptor
125
139
  #
126
140
  # @rbs (Integer backlog) -> void
127
141
  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)
142
+ load = backlog + accept_queue_length
143
+ @accepted_load_value.setbyte(0, load & 0xff)
144
+ @accepted_load_value.setbyte(1, (load >> 8) & 0xff)
145
+ @accepted_load_value.setbyte(2, (load >> 16) & 0xff)
146
+ @accepted_load_value.setbyte(3, (load >> 24) & 0xff)
132
147
  LibBPFRuby.map_update(@loads_fd, @load_key, @accepted_load_value)
133
148
  end
134
149
 
135
- # Marks a worker's slot as unavailable by parking its load at the
136
- # sentinel maximum so the BPF program's least-loaded pick skips it.
150
+ # Parks a worker's load at the maximum so BPF favors other workers.
151
+ # Stop publishing its load first to avoid overwriting the sentinel.
137
152
  #
138
153
  # @param worker_index [Integer] slot index for the worker to mark
139
154
  # @return [void]
140
155
  #
141
156
  # @rbs (Integer worker_index) -> void
142
157
  def self.mark_unavailable(worker_index)
143
- return unless @loads_fd
158
+ LibBPFRuby.map_update(@loads_fd, [worker_index + 1].pack("L"), [UNAVAILABLE_LOAD].pack("L"))
159
+ end
144
160
 
145
- LibBPFRuby.map_update(@loads_fd, [worker_index + 1].pack("L"), [0xffffffff].pack("L"))
161
+ # Returns the number of connections waiting in this worker's accept
162
+ # queues, which the reactor backlog cannot see.
163
+ #
164
+ # @return [Integer]
165
+ #
166
+ # @rbs () -> Integer
167
+ def self.accept_queue_length
168
+ @worker_listeners.sum { |listener| listener.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).data.unpack1("L", offset: TCP_INFO_UNACKED_OFFSET) }
146
169
  end
170
+ private_class_method :accept_queue_length
147
171
  end
148
172
  end
data/lib/raptor/server.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  # rbs_inline: enabled
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "openssl"
4
5
  require "socket"
5
6
 
6
7
  require "atomic-ruby/atomic_boolean"
7
8
 
9
+ require_relative "binder"
10
+ require_relative "log"
11
+ require_relative "raptor_native"
8
12
  require_relative "reuseport_bpf"
9
13
 
10
14
  module Raptor
11
- # Accepts client connections on TCP, Unix, and SSL listeners and
12
- # dispatches them into the request pipeline. Yields between accepts
13
- # when the thread pool queue exceeds the pool size so overloaded
14
- # workers stay responsive to in-flight requests.
15
+ # Accepts client connections on TCP, Unix, SSL, and cleartext HTTP/2
16
+ # listeners and dispatches them into the request pipeline.
15
17
  #
16
18
  class Server
17
19
  HTTP_SCHEME = "http"
@@ -25,42 +27,44 @@ module Raptor
25
27
  BACKPRESSURE_THRESHOLD_MULTIPLIER = 1.2
26
28
  MIN_BACKPRESSURE_THRESHOLD = 8
27
29
 
28
- # @rbs @binder: Binder
29
- # @rbs @listeners: Array[TCPServer | UNIXServer | Binder::SslListener]
30
30
  # @rbs @reactor: Reactor
31
31
  # @rbs @thread_pool: AtomicThreadPool
32
32
  # @rbs @http1: Http1
33
33
  # @rbs @http2: Http2
34
34
  # @rbs @first_data_timeout: Integer
35
+ # @rbs @listeners: Array[TCPServer | UNIXServer | Binder::SslListener | Binder::H2cListener]
35
36
  # @rbs @drain_accept_queue: bool
36
- # @rbs @bpf_active: bool
37
+ # @rbs @worker_index: Integer?
37
38
  # @rbs @running: AtomicBoolean
39
+ # @rbs @thread: Thread?
40
+ # @rbs @load_reporter: Thread?
38
41
 
39
- # Creates a new Server instance.
42
+ # Creates a new Server.
40
43
  #
41
- # @param binder [Binder] the binder managing listening sockets
42
44
  # @param reactor [Reactor] the reactor for handling client connections
43
45
  # @param thread_pool [AtomicThreadPool] thread pool for application processing
44
46
  # @param http1 [Http1] the HTTP/1.1 handler
45
- # @param http2 [Http2] the HTTP/2 handler (provides the initial SETTINGS frame)
47
+ # @param http2 [Http2] the HTTP/2 handler
46
48
  # @param connection_options [Hash] per-connection timeout configuration, used to bound TLS handshakes
47
- # @param listeners [Array] the per-worker listeners this server accepts on
49
+ # @option connection_options [Integer] :first_data_timeout timeout for completing the TLS handshake
50
+ # @param listeners [Array<TCPServer, UNIXServer, Binder::SslListener, Binder::H2cListener>] the per-worker listeners this server accepts on
48
51
  # @param drain_accept_queue [Boolean] whether to drain the kernel accept queue on shutdown
49
- # @param worker_index [Integer, nil] the slot index for BPF load reporting
52
+ # @param worker_index [Integer, nil] this worker's BPF slot index, or nil when BPF dispatch is inactive
50
53
  # @return [void]
51
54
  #
52
- # @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
53
- def initialize(binder, reactor, thread_pool, http1, http2, connection_options:, listeners:, drain_accept_queue: false, worker_index: nil)
54
- @binder = binder
55
- @listeners = listeners
55
+ # @rbs (Reactor reactor, AtomicThreadPool thread_pool, Http1 http1, Http2 http2, connection_options: Hash[Symbol, untyped], listeners: Array[TCPServer | UNIXServer | Binder::SslListener | Binder::H2cListener], ?drain_accept_queue: bool, ?worker_index: Integer?) -> void
56
+ def initialize(reactor, thread_pool, http1, http2, connection_options:, listeners:, drain_accept_queue: false, worker_index: nil)
56
57
  @reactor = reactor
57
58
  @thread_pool = thread_pool
58
59
  @http1 = http1
59
60
  @http2 = http2
60
61
  @first_data_timeout = connection_options[:first_data_timeout]
62
+ @listeners = listeners
61
63
  @drain_accept_queue = drain_accept_queue
62
- @bpf_active = !!worker_index
64
+ @worker_index = worker_index
63
65
  @running = AtomicBoolean.new(true)
66
+ @thread = nil
67
+ @load_reporter = nil
64
68
  end
65
69
 
66
70
  # Starts the server's main accept loop in a new thread.
@@ -69,30 +73,36 @@ module Raptor
69
73
  #
70
74
  # @rbs () -> Thread
71
75
  def run
72
- spawn_load_reporter if @bpf_active
76
+ @load_reporter = spawn_load_reporter if @worker_index
73
77
 
74
- Thread.new do
78
+ @thread = Thread.new do
75
79
  Thread.current.name = "Server"
76
80
 
77
- backpressure_threshold = [(@thread_pool.size * BACKPRESSURE_THRESHOLD_MULTIPLIER).ceil, MIN_BACKPRESSURE_THRESHOLD].max
78
-
79
81
  while @running.true?
80
82
  begin
81
- ready_servers, _, _ = IO.select(@listeners, nil, nil, 1)
83
+ ready_servers, = IO.select(@listeners, nil, nil, 1)
82
84
  rescue IOError, Errno::EBADF
83
85
  break
84
86
  end
85
87
 
86
88
  next unless ready_servers
87
- next if @reactor.backlog >= backpressure_threshold
88
89
 
89
- if @thread_pool.queue_size > @thread_pool.size
90
- Thread.pass
90
+ backpressure_threshold = [(@thread_pool.size * BACKPRESSURE_THRESHOLD_MULTIPLIER).ceil, MIN_BACKPRESSURE_THRESHOLD].max
91
+ if @reactor.backlog >= backpressure_threshold || @thread_pool.queue_size > @thread_pool.size
92
+ CPU.yield
91
93
  next
92
94
  end
93
95
 
96
+ break if @running.false?
97
+
94
98
  ready_servers.each { |listener| accept_connection(listener) }
95
99
  end
100
+
101
+ if @worker_index
102
+ # Stop load updates before parking the slot so the sentinel survives.
103
+ @load_reporter.join
104
+ ReuseportBPF.mark_unavailable(@worker_index)
105
+ end
96
106
  end
97
107
  end
98
108
 
@@ -105,6 +115,7 @@ module Raptor
105
115
  # @rbs () -> void
106
116
  def stop_accepting
107
117
  @running.make_false
118
+ @thread&.wakeup rescue nil
108
119
  drain_accept_queue if @drain_accept_queue
109
120
  end
110
121
 
@@ -121,12 +132,12 @@ module Raptor
121
132
 
122
133
  private
123
134
 
124
- # Starts a background thread that publishes this worker's reactor
125
- # backlog to the BPF map for load-aware dispatch.
135
+ # Starts a background thread that publishes this worker's load to the
136
+ # BPF map for load-aware dispatch.
126
137
  #
127
- # @return [void]
138
+ # @return [Thread] the load reporter
128
139
  #
129
- # @rbs () -> void
140
+ # @rbs () -> Thread
130
141
  def spawn_load_reporter
131
142
  Thread.new do
132
143
  Thread.current.name = "Load Reporter"
@@ -154,12 +165,12 @@ module Raptor
154
165
 
155
166
  # Accepts a connection from the given listener, immediately publishes
156
167
  # the pending work to the BPF map, and dispatches the socket into the
157
- # HTTP/1.1 or SSL/HTTP-2 pipeline.
168
+ # SSL, cleartext HTTP/2, or HTTP/1.1 pipeline.
158
169
  #
159
- # @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
170
+ # @param listener [TCPServer, UNIXServer, Binder::SslListener, Binder::H2cListener] the ready listener
160
171
  # @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
161
172
  #
162
- # @rbs (TCPServer | UNIXServer | Binder::SslListener listener) -> bool
173
+ # @rbs (TCPServer | UNIXServer | Binder::SslListener | Binder::H2cListener listener) -> bool
163
174
  def accept_connection(listener)
164
175
  tcp_client = begin
165
176
  listener.is_a?(Binder::SslListener) ? listener.tcp_server.accept_nonblock : listener.accept_nonblock
@@ -167,13 +178,14 @@ module Raptor
167
178
  return false
168
179
  end
169
180
 
170
- if tcp_client.is_a?(TCPSocket)
171
- remote_addr = tcp_client.remote_address.ip_address
172
- else
173
- remote_addr = DEFAULT_REMOTE_ADDR
181
+ remote_addr = begin
182
+ tcp_client.is_a?(TCPSocket) ? tcp_client.remote_address.ip_address : DEFAULT_REMOTE_ADDR
183
+ rescue SystemCallError
184
+ tcp_client.close
185
+ return true
174
186
  end
175
187
 
176
- ReuseportBPF.update_accepted_load(@reactor.backlog + 1) if @bpf_active
188
+ ReuseportBPF.update_accepted_load(@reactor.backlog + 1) if @worker_index && @running.true?
177
189
 
178
190
  if listener.is_a?(Binder::SslListener)
179
191
  @thread_pool << proc do
@@ -211,7 +223,7 @@ module Raptor
211
223
  #
212
224
  # @param listener [Binder::SslListener] the SSL listener that accepted the connection
213
225
  # @param tcp_client [TCPSocket] the accepted TCP socket
214
- # @param remote_addr [String] the client's IP address
226
+ # @param remote_addr [String] the client IP address
215
227
  # @return [void]
216
228
  #
217
229
  # @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr) -> void
@@ -268,6 +280,9 @@ module Raptor
268
280
  Log.rescued_error(error)
269
281
  ssl_socket.close rescue nil
270
282
  false
283
+ rescue SystemCallError
284
+ ssl_socket.close rescue nil
285
+ false
271
286
  end
272
287
  end
273
288
 
data/lib/raptor/stats.rb CHANGED
@@ -27,7 +27,8 @@ module Raptor
27
27
  # @rbs @num_workers: Integer
28
28
  # @rbs @mmap: untyped
29
29
 
30
- # Allocates the shared mmap region for `num_workers` slots.
30
+ # Creates a new Stats and allocates the shared mmap region for
31
+ # `num_workers` slots.
31
32
  #
32
33
  # @param num_workers [Integer] number of worker slots to allocate
33
34
  # @return [void]
@@ -80,14 +81,25 @@ module Raptor
80
81
 
81
82
  # Reads stats for a worker slot from shared memory.
82
83
  #
83
- # @param slot [Integer] slot offset to read from
84
+ # @param slot [Integer] slot index to read from
84
85
  # @return [Hash] stat hash with :pid, :index, :phase, :requests, :backlog, :busy_threads, :thread_capacity, :started_at, :last_checkin, and :booted
85
86
  #
86
87
  # @rbs (Integer slot) -> Hash[Symbol, untyped]
87
88
  def read(slot)
88
89
  data = @mmap[slot * SLOT_SIZE, SLOT_SIZE]
89
90
  pid, index, phase, requests, backlog, busy_threads, thread_capacity, started_at, last_checkin, booted = data.unpack(SLOT_FORMAT)
90
- { pid:, index:, phase:, requests:, backlog:, busy_threads:, thread_capacity:, started_at:, last_checkin:, booted: booted == 1 }
91
+ {
92
+ pid: pid,
93
+ index: index,
94
+ phase: phase,
95
+ requests: requests,
96
+ backlog: backlog,
97
+ busy_threads: busy_threads,
98
+ thread_capacity: thread_capacity,
99
+ started_at: started_at,
100
+ last_checkin: last_checkin,
101
+ booted: booted == 1
102
+ }
91
103
  end
92
104
  end
93
105
  end
@@ -15,7 +15,8 @@ module Raptor
15
15
  # Returns a reusable value stored on the current thread.
16
16
  #
17
17
  # @param key [Symbol] thread-variable key used to cache the value
18
- # @yieldreturn [Object] value to store when the key is unset
18
+ # @yield when the key is unset
19
+ # @yieldreturn [Object] the value to store
19
20
  # @return [Object] the existing or newly stored value
20
21
  #
21
22
  # @rbs (Symbol key) { () -> untyped } -> untyped
@@ -24,8 +25,8 @@ module Raptor
24
25
  thread.thread_variable_get(key) || thread.thread_variable_set(key, yield)
25
26
  end
26
27
 
27
- # Clears application-owned true thread locals while retaining Raptor's
28
- # reusable per-thread values.
28
+ # Clears application thread locals while retaining Raptor's reusable
29
+ # per-thread values.
29
30
  #
30
31
  # @return [void]
31
32
  #
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.22.2"
5
+ VERSION = "0.23.0"
6
6
  end
@@ -11,7 +11,8 @@ module Rackup
11
11
  #
12
12
  # @param app [#call] the Rack application to serve
13
13
  # @param options [Hash] handler options provided by Rackup or the host
14
- # @yield [cluster] the cluster instance, before it starts running
14
+ # @yield before the cluster starts running
15
+ # @yieldparam cluster [::Raptor::Cluster] the cluster instance
15
16
  # @return [void]
16
17
  #
17
18
  # @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
@@ -52,17 +52,17 @@ module Raptor
52
52
  # Returns the array of bind URIs.
53
53
  #
54
54
  # @return [Array<String>]
55
- attr_reader bind_uris: untyped
55
+ attr_reader bind_uris: Array[String]
56
56
 
57
57
  # Returns the kernel `listen()` queue depth for TCP/SSL listeners.
58
58
  #
59
59
  # @return [Integer]
60
- attr_reader socket_backlog: untyped
60
+ attr_reader socket_backlog: Integer
61
61
 
62
62
  # Returns the array of listening sockets.
63
63
  #
64
64
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
65
- attr_reader listeners: untyped
65
+ attr_reader listeners: Array[TCPServer | UNIXServer | SslListener | H2cListener]
66
66
 
67
67
  # Creates a new Binder and binds each URI. `localhost` expands to both
68
68
  # IPv4 and IPv6 loopback; when `inherited_fds` supplies file descriptors
@@ -86,8 +86,8 @@ module Raptor
86
86
  # @rbs () -> Array[String]
87
87
  def addresses: () -> Array[String]
88
88
 
89
- # Returns the port of the first TCP or SSL listener, or 0 when none
90
- # is configured.
89
+ # Returns the port of the first TCP, SSL, or cleartext HTTP/2 listener,
90
+ # or 0 when none is configured.
91
91
  #
92
92
  # @return [Integer]
93
93
  #
@@ -136,7 +136,7 @@ module Raptor
136
136
 
137
137
  # Creates fresh listeners for the given URI.
138
138
  #
139
- # @param uri [URI] the parsed bind URI
139
+ # @param uri [URI::Generic] the parsed bind URI
140
140
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
141
141
  # @raise [UnknownBindSchemeError] if the URI scheme is not supported
142
142
  #
@@ -146,7 +146,7 @@ module Raptor
146
146
  # Reconstructs listeners for the given URI from inherited file
147
147
  # descriptors.
148
148
  #
149
- # @param uri [URI] the parsed bind URI the FDs were bound to
149
+ # @param uri [URI::Generic] the parsed bind URI the FDs were bound to
150
150
  # @param filenos [Array<Integer>] file descriptors to wrap
151
151
  # @return [Array<TCPServer, UNIXServer, SslListener, H2cListener>]
152
152
  # @raise [UnknownBindSchemeError] if the URI scheme is not supported
@@ -188,7 +188,7 @@ module Raptor
188
188
  #
189
189
  # @param host [String, nil] hostname or IP address to bind to
190
190
  # @param port [Integer, nil] port number to bind to
191
- # @param ssl_params [Hash<String, String>] SSL options (`cert` and `key` file paths)
191
+ # @param ssl_params [Hash{String => String}] SSL options (`cert` and `key` file paths)
192
192
  # @return [Array<SslListener>] array containing the created SSL listener(s)
193
193
  #
194
194
  # @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
@@ -197,7 +197,7 @@ module Raptor
197
197
  # Builds a frozen `OpenSSL::SSL::SSLContext` configured for HTTP/2 and
198
198
  # HTTP/1.1 ALPN negotiation.
199
199
  #
200
- # @param ssl_params [Hash<String, String>] SSL options ("cert" and "key" paths)
200
+ # @param ssl_params [Hash{String => String}] SSL options (`cert` and `key` file paths)
201
201
  # @return [OpenSSL::SSL::SSLContext]
202
202
  #
203
203
  # @rbs (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext
@@ -57,17 +57,17 @@ module Raptor
57
57
 
58
58
  @parser: OptionParser
59
59
 
60
- @options: Hash[Symbol, untyped]
61
-
62
60
  @command: Symbol
63
61
 
64
- # Creates a new CLI instance from `argv`. A rackup file may be given
65
- # as the first positional argument; every other value is parsed as a
66
- # flag.
62
+ @options: Hash[Symbol, untyped]
63
+
64
+ # Creates a new CLI from `argv`. A rackup file may be given as the
65
+ # first positional argument; every other value is parsed as a flag.
67
66
  #
68
67
  # @param argv [Array<String>] command-line arguments to parse
69
68
  # @return [void]
70
69
  # @raise [OptionParser::ParseError] if invalid options are provided
70
+ # @raise [ArgumentError] if the config file or max threads value is invalid
71
71
  #
72
72
  # @rbs (Array[String] argv) -> void
73
73
  def initialize: (Array[String] argv) -> void
@@ -88,7 +88,7 @@ module Raptor
88
88
  # @rbs () -> void
89
89
  def run_stats: () -> void
90
90
 
91
- # Returns the path from a `-c`/`--config` flag in `argv`, recognising
91
+ # Returns the path from a `-c`/`--config` flag in `argv`, recognizing
92
92
  # all four OptionParser-accepted forms (`-c PATH`, `-cPATH`,
93
93
  # `--config PATH`, `--config=PATH`).
94
94
  #
@@ -62,7 +62,7 @@ module Raptor
62
62
 
63
63
  @launch_argv: Array[String]?
64
64
 
65
- @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
65
+ @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
66
66
 
67
67
  @binder: Binder
68
68
 
@@ -104,7 +104,7 @@ module Raptor
104
104
 
105
105
  @seed_pid: Integer?
106
106
 
107
- @refork_threshold_idx: Integer
107
+ @refork_threshold_index: Integer
108
108
 
109
109
  @refork_requested: bool
110
110
 
@@ -210,19 +210,19 @@ module Raptor
210
210
  # @rbs () -> Array[Hash[Symbol, untyped]]
211
211
  def stats: () -> Array[Hash[Symbol, untyped]]
212
212
 
213
- # Returns cluster statistics for the control server.
214
- # For an adaptive pool, `max_threads` is the number of threads currently
215
- # running so utilization reflects the capacity available at that moment.
213
+ private
214
+
215
+ # Returns cluster statistics for the control server. For an adaptive
216
+ # pool, `max_threads` is the number of threads currently running so
217
+ # utilization reflects the capacity available at that moment.
216
218
  #
217
219
  # @return [Hash] cluster statistics
218
220
  #
219
221
  # @rbs () -> Hash[Symbol, untyped]
220
222
  def stats_hash: () -> Hash[Symbol, untyped]
221
223
 
222
- private
223
-
224
- # @rbs (Float timestamp) -> String
225
- def timestamp: (Float timestamp) -> String
224
+ # @rbs (Numeric time) -> String
225
+ def timestamp: (Numeric time) -> String
226
226
 
227
227
  # Returns the inherited-FDs hash for a systemd socket-activation handoff,
228
228
  # pairing each bind URI with the FD systemd passed at the same index.
@@ -15,9 +15,10 @@ module Raptor
15
15
 
16
16
  @thread: Thread?
17
17
 
18
- # Creates a control server for `url` without binding it.
18
+ # Creates a new ControlServer for `url` without binding it.
19
19
  #
20
20
  # @param url [String] `unix://` URL to listen on
21
+ # @yield for each stats request
21
22
  # @yieldreturn [Hash] cluster statistics
22
23
  # @return [void]
23
24
  # @raise [ArgumentError] if the URL is not a Unix socket
@@ -57,6 +57,7 @@ module Raptor
57
57
  #
58
58
  # @param max_buffer_size [Integer] maximum body bytes waiting to be written
59
59
  # @return [void]
60
+ # @raise [ArgumentError] if `max_buffer_size` is not positive
60
61
  #
61
62
  # @rbs (?max_buffer_size: Integer) -> void
62
63
  def initialize: (?max_buffer_size: Integer) -> void
@@ -64,6 +65,7 @@ module Raptor
64
65
  # Registers a callback that runs on the request thread once the server
65
66
  # accepts the response stream.
66
67
  #
68
+ # @yield once the response stream opens
67
69
  # @yieldparam stream [DetachedBody] the opened response stream
68
70
  # @return [DetachedBody]
69
71
  #
@@ -72,6 +74,7 @@ module Raptor
72
74
 
73
75
  # Registers a callback for when the response stream closes.
74
76
  #
77
+ # @yield once the response stream closes
75
78
  # @yieldparam reason [Symbol] why the stream closed
76
79
  # @return [DetachedBody]
77
80
  #
@@ -82,11 +85,12 @@ module Raptor
82
85
  #
83
86
  # @param chunk [String] response body bytes
84
87
  # @return [Symbol] `:accepted`, `:full`, or `:closed`
88
+ # @raise [TypeError] if the chunk is not a String
85
89
  #
86
90
  # @rbs (String chunk) -> Symbol
87
91
  def try_write: (String chunk) -> Symbol
88
92
 
89
- # Finishes the body after all accepted bytes have been written.
93
+ # Marks the body complete; the stream finishes once buffered bytes are written.
90
94
  #
91
95
  # @param trailers [Hash] trailing response headers
92
96
  # @return [void]