raptor 0.7.0 → 0.8.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/CHANGELOG.md +19 -0
- data/README.md +148 -23
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +122 -28
- data/lib/raptor/cli.rb +82 -21
- data/lib/raptor/cluster.rb +188 -32
- data/lib/raptor/http.rb +75 -0
- data/lib/raptor/{request.rb → http1.rb} +200 -79
- data/lib/raptor/http2.rb +85 -40
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/server.rb +57 -37
- data/lib/raptor/stats.rb +1 -1
- data/lib/raptor/systemd.rb +69 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +72 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +89 -13
- data/sig/generated/raptor/http.rbs +52 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +107 -31
- data/sig/generated/raptor/http2.rbs +46 -13
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/server.rbs +32 -18
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +7 -3
data/lib/raptor/http2.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "stringio"
|
|
|
6
6
|
require "atomic-ruby/atom"
|
|
7
7
|
require "rack"
|
|
8
8
|
|
|
9
|
-
require_relative "
|
|
9
|
+
require_relative "http"
|
|
10
10
|
require_relative "raptor_http2"
|
|
11
11
|
|
|
12
12
|
module Raptor
|
|
@@ -27,12 +27,17 @@ module Raptor
|
|
|
27
27
|
IDLE = :idle
|
|
28
28
|
|
|
29
29
|
# @rbs @state: Atom
|
|
30
|
+
# @rbs @write_timeout: Integer
|
|
30
31
|
|
|
31
32
|
# Creates a new Writer.
|
|
32
33
|
#
|
|
33
|
-
# @
|
|
34
|
-
|
|
34
|
+
# @param write_timeout [Integer] per-write socket timeout passed through to {Http.socket_write}
|
|
35
|
+
# @return [void]
|
|
36
|
+
#
|
|
37
|
+
# @rbs (write_timeout: Integer) -> void
|
|
38
|
+
def initialize(write_timeout:)
|
|
35
39
|
@state = Atom.new(IDLE)
|
|
40
|
+
@write_timeout = write_timeout
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
# Writes frames to the socket, coordinating with concurrent writers
|
|
@@ -69,7 +74,7 @@ module Raptor
|
|
|
69
74
|
break if pending.empty?
|
|
70
75
|
|
|
71
76
|
pending.each do |frame|
|
|
72
|
-
|
|
77
|
+
Http.socket_write(socket, frame, timeout: @write_timeout) rescue nil
|
|
73
78
|
end
|
|
74
79
|
end
|
|
75
80
|
end
|
|
@@ -78,7 +83,7 @@ module Raptor
|
|
|
78
83
|
# Per-connection outbound flow-control accounting.
|
|
79
84
|
#
|
|
80
85
|
# Tracks the peer's connection-level and per-stream receive windows so
|
|
81
|
-
# outbound DATA frames respect RFC 7540
|
|
86
|
+
# outbound DATA frames respect RFC 7540 section 5.2. Threads dispatching stream
|
|
82
87
|
# responses call `acquire` to reserve send capacity; threads applying
|
|
83
88
|
# inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
|
|
84
89
|
# replenish it. The connection window and per-stream windows live in
|
|
@@ -146,9 +151,9 @@ module Raptor
|
|
|
146
151
|
end
|
|
147
152
|
|
|
148
153
|
if granted > 0
|
|
149
|
-
@stream_windows.swap do |
|
|
150
|
-
current =
|
|
151
|
-
|
|
154
|
+
@stream_windows.swap do |windows|
|
|
155
|
+
current = windows[stream_id] || initial
|
|
156
|
+
windows.merge(stream_id => current - granted)
|
|
152
157
|
end
|
|
153
158
|
return granted
|
|
154
159
|
end
|
|
@@ -178,14 +183,14 @@ module Raptor
|
|
|
178
183
|
# @rbs (Integer stream_id, Integer increment) -> void
|
|
179
184
|
def add_stream_window(stream_id, increment)
|
|
180
185
|
initial = @initial_stream_window.value
|
|
181
|
-
@stream_windows.swap do |
|
|
182
|
-
current =
|
|
183
|
-
|
|
186
|
+
@stream_windows.swap do |windows|
|
|
187
|
+
current = windows[stream_id] || initial
|
|
188
|
+
windows.merge(stream_id => current + increment)
|
|
184
189
|
end
|
|
185
190
|
end
|
|
186
191
|
|
|
187
192
|
# Updates the peer's `SETTINGS_INITIAL_WINDOW_SIZE`. Shifts every
|
|
188
|
-
# existing stream window by the delta as required by RFC 7540
|
|
193
|
+
# existing stream window by the delta as required by RFC 7540 section 6.9.2.
|
|
189
194
|
#
|
|
190
195
|
# @param new_size [Integer] the peer's new initial window size
|
|
191
196
|
# @return [void]
|
|
@@ -197,8 +202,8 @@ module Raptor
|
|
|
197
202
|
delta = new_size - old
|
|
198
203
|
return if delta.zero?
|
|
199
204
|
|
|
200
|
-
@stream_windows.swap do |
|
|
201
|
-
|
|
205
|
+
@stream_windows.swap do |windows|
|
|
206
|
+
windows.transform_values { |size| size + delta }
|
|
202
207
|
end
|
|
203
208
|
end
|
|
204
209
|
|
|
@@ -213,12 +218,12 @@ module Raptor
|
|
|
213
218
|
def discard_stream(stream_id)
|
|
214
219
|
return unless @stream_windows.value.key?(stream_id)
|
|
215
220
|
|
|
216
|
-
@stream_windows.swap do |
|
|
217
|
-
next
|
|
221
|
+
@stream_windows.swap do |windows|
|
|
222
|
+
next windows unless windows.key?(stream_id)
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
224
|
+
pruned = windows.dup
|
|
225
|
+
pruned.delete(stream_id)
|
|
226
|
+
pruned
|
|
222
227
|
end
|
|
223
228
|
end
|
|
224
229
|
end
|
|
@@ -244,34 +249,51 @@ module Raptor
|
|
|
244
249
|
|
|
245
250
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
246
251
|
# @rbs @server_port: Integer
|
|
252
|
+
# @rbs @write_timeout: Integer
|
|
253
|
+
# @rbs @access_log_io: IO?
|
|
247
254
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
255
|
+
# @rbs @initial_settings_frame: String
|
|
256
|
+
|
|
257
|
+
# The initial server SETTINGS frame sent on every new HTTP/2 connection.
|
|
258
|
+
#
|
|
259
|
+
# @return [String] the encoded SETTINGS frame
|
|
260
|
+
attr_reader :initial_settings_frame
|
|
248
261
|
|
|
249
262
|
# Creates a new Http2 handler.
|
|
250
263
|
#
|
|
251
264
|
# @param app [#call] the Rack application to dispatch requests to
|
|
252
265
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
266
|
+
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
267
|
+
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
268
|
+
# @param http2_options [Hash] HTTP/2-specific settings
|
|
269
|
+
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
270
|
+
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
253
271
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
254
272
|
# @return [void]
|
|
255
273
|
#
|
|
256
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
257
|
-
def initialize(app, server_port, on_error: nil)
|
|
274
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
275
|
+
def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, on_error: nil)
|
|
258
276
|
@app = app
|
|
259
277
|
@server_port = server_port
|
|
278
|
+
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
279
|
+
@access_log_io = access_log_io
|
|
260
280
|
@on_error = on_error
|
|
261
|
-
end
|
|
262
281
|
|
|
263
|
-
# Builds the initial server SETTINGS frame to send on connection establishment.
|
|
264
|
-
#
|
|
265
|
-
# @return [String] the encoded SETTINGS frame
|
|
266
|
-
#
|
|
267
|
-
# @rbs () -> String
|
|
268
|
-
def self.build_server_settings_frame
|
|
269
282
|
parser = Http2Parser.new
|
|
270
283
|
settings_payload = parser.build_settings(
|
|
271
|
-
max_concurrent_streams:
|
|
284
|
+
max_concurrent_streams: http2_options[:max_concurrent_streams],
|
|
272
285
|
initial_window_size: DEFAULT_WINDOW_SIZE
|
|
273
286
|
)
|
|
274
|
-
parser.build_frame(:settings, 0, 0, settings_payload)
|
|
287
|
+
@initial_settings_frame = parser.build_frame(:settings, 0, 0, settings_payload).freeze
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Creates a per-connection {Writer} configured with the handler's write timeout.
|
|
291
|
+
#
|
|
292
|
+
# @return [Writer] a new per-connection frame writer
|
|
293
|
+
#
|
|
294
|
+
# @rbs () -> Writer
|
|
295
|
+
def create_writer
|
|
296
|
+
Writer.new(write_timeout: @write_timeout)
|
|
275
297
|
end
|
|
276
298
|
|
|
277
299
|
# Processes HTTP/2 frames from the connection buffer.
|
|
@@ -626,7 +648,8 @@ module Raptor
|
|
|
626
648
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
627
649
|
status, response_headers, response_body = @app.call(env)
|
|
628
650
|
|
|
629
|
-
write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
|
|
651
|
+
response_size = write_http2_response(socket, writer, flow_control, stream_id, status, response_headers, response_body)
|
|
652
|
+
write_access_log(env, status, response_size, remote_addr) if @access_log_io
|
|
630
653
|
rescue => error
|
|
631
654
|
write_http2_error_response(socket, writer, stream_id)
|
|
632
655
|
|
|
@@ -652,9 +675,9 @@ module Raptor
|
|
|
652
675
|
# @param status [Integer] HTTP status code
|
|
653
676
|
# @param headers [Hash] response headers from the Rack application
|
|
654
677
|
# @param body [Object] response body responding to each
|
|
655
|
-
# @return [
|
|
678
|
+
# @return [String] the response body size in bytes
|
|
656
679
|
#
|
|
657
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) ->
|
|
680
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> String
|
|
658
681
|
def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
|
|
659
682
|
parser = Http2Parser.new
|
|
660
683
|
|
|
@@ -673,11 +696,16 @@ module Raptor
|
|
|
673
696
|
|
|
674
697
|
encoded_headers = parser.encode_headers(header_pairs)
|
|
675
698
|
body_chunks = []
|
|
676
|
-
|
|
699
|
+
body_bytes = 0
|
|
700
|
+
body.each do |chunk|
|
|
701
|
+
next if chunk.empty?
|
|
702
|
+
body_chunks << chunk
|
|
703
|
+
body_bytes += chunk.bytesize
|
|
704
|
+
end
|
|
677
705
|
|
|
678
706
|
if body_chunks.empty?
|
|
679
707
|
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
|
|
680
|
-
return
|
|
708
|
+
return "0"
|
|
681
709
|
end
|
|
682
710
|
|
|
683
711
|
frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
|
|
@@ -697,6 +725,7 @@ module Raptor
|
|
|
697
725
|
end
|
|
698
726
|
|
|
699
727
|
writer.write_frames(socket, frames)
|
|
728
|
+
body_bytes.to_s
|
|
700
729
|
end
|
|
701
730
|
|
|
702
731
|
# Writes a 500 error response as HTTP/2 frames.
|
|
@@ -717,6 +746,20 @@ module Raptor
|
|
|
717
746
|
)
|
|
718
747
|
end
|
|
719
748
|
|
|
749
|
+
# Instance-level wrapper around {Http.write_access_log} that routes to
|
|
750
|
+
# the configured `@access_log_io`.
|
|
751
|
+
#
|
|
752
|
+
# @param env [Hash] the Rack environment
|
|
753
|
+
# @param status [Integer] the response status code
|
|
754
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
755
|
+
# @param remote_addr [String] the client IP address
|
|
756
|
+
# @return [void]
|
|
757
|
+
#
|
|
758
|
+
# @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
759
|
+
def write_access_log(env, status, size, remote_addr)
|
|
760
|
+
Http.write_access_log(@access_log_io, env, status, size, remote_addr)
|
|
761
|
+
end
|
|
762
|
+
|
|
720
763
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
|
721
764
|
#
|
|
722
765
|
# Translates HTTP/2 pseudo-headers into Rack-compatible environment keys
|
|
@@ -743,9 +786,9 @@ module Raptor
|
|
|
743
786
|
when ":authority" then env[Rack::HTTP_HOST] = value
|
|
744
787
|
end
|
|
745
788
|
elsif name == "content-type"
|
|
746
|
-
env[
|
|
789
|
+
env[Http::CONTENT_TYPE] = value
|
|
747
790
|
elsif name == "content-length"
|
|
748
|
-
env[
|
|
791
|
+
env[Http::CONTENT_LENGTH] = value
|
|
749
792
|
else
|
|
750
793
|
rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
751
794
|
env[rack_key] = value
|
|
@@ -763,11 +806,13 @@ module Raptor
|
|
|
763
806
|
env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
|
|
764
807
|
env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
|
|
765
808
|
|
|
766
|
-
if body.bytesize.positive? && !env.key?(
|
|
767
|
-
env[
|
|
809
|
+
if body.bytesize.positive? && !env.key?(Http::CONTENT_LENGTH)
|
|
810
|
+
env[Http::CONTENT_LENGTH] = body.bytesize.to_s
|
|
768
811
|
end
|
|
769
812
|
|
|
770
|
-
env[
|
|
813
|
+
env[Http::REMOTE_ADDR] = remote_addr
|
|
814
|
+
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
815
|
+
env[Http::HTTP_VERSION] = SERVER_PROTOCOL
|
|
771
816
|
|
|
772
817
|
populate_server_name_and_port(env)
|
|
773
818
|
|
data/lib/raptor/reactor.rb
CHANGED
|
@@ -14,10 +14,12 @@ module Raptor
|
|
|
14
14
|
# the server uses for backpressure control to prevent overload.
|
|
15
15
|
#
|
|
16
16
|
# @example
|
|
17
|
-
# reactor = Reactor.new(
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# reactor = Reactor.new(
|
|
18
|
+
# ractor_pool,
|
|
19
|
+
# thread_pool,
|
|
20
|
+
# connection_options: { first_data_timeout: 30, chunk_data_timeout: 10 },
|
|
21
|
+
# http1_options: { persistent_data_timeout: 65 }
|
|
22
|
+
# )
|
|
21
23
|
# reactor.run
|
|
22
24
|
# reactor.add(id: client.object_id, socket: client)
|
|
23
25
|
# # ... later
|
|
@@ -68,7 +70,9 @@ module Raptor
|
|
|
68
70
|
|
|
69
71
|
# @rbs @thread_pool: untyped
|
|
70
72
|
# @rbs @ractor_pool: untyped
|
|
71
|
-
# @rbs @
|
|
73
|
+
# @rbs @first_data_timeout: Integer
|
|
74
|
+
# @rbs @chunk_data_timeout: Integer
|
|
75
|
+
# @rbs @persistent_data_timeout: Integer
|
|
72
76
|
# @rbs @selector: NIO::Selector
|
|
73
77
|
# @rbs @queue: Queue[TCPSocket]
|
|
74
78
|
# @rbs @timeouts: RedBlackTree[TimeoutClient]
|
|
@@ -82,17 +86,20 @@ module Raptor
|
|
|
82
86
|
#
|
|
83
87
|
# @param ractor_pool [RactorPool] ractor pool for HTTP parsing
|
|
84
88
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
85
|
-
# @param
|
|
86
|
-
# @option
|
|
87
|
-
# @option
|
|
88
|
-
# @
|
|
89
|
+
# @param connection_options [Hash] per-connection timeout configuration
|
|
90
|
+
# @option connection_options [Integer] :first_data_timeout timeout for initial data
|
|
91
|
+
# @option connection_options [Integer] :chunk_data_timeout timeout for subsequent chunks
|
|
92
|
+
# @param http1_options [Hash] HTTP/1.1-specific configuration
|
|
93
|
+
# @option http1_options [Integer] :persistent_data_timeout timeout for keep-alive idle connections
|
|
89
94
|
# @return [void]
|
|
90
95
|
#
|
|
91
|
-
# @rbs (untyped ractor_pool, untyped thread_pool,
|
|
92
|
-
def initialize(ractor_pool, thread_pool,
|
|
96
|
+
# @rbs (untyped ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
|
|
97
|
+
def initialize(ractor_pool, thread_pool, connection_options:, http1_options:)
|
|
93
98
|
@ractor_pool = ractor_pool
|
|
94
99
|
@thread_pool = thread_pool
|
|
95
|
-
@
|
|
100
|
+
@first_data_timeout = connection_options[:first_data_timeout]
|
|
101
|
+
@chunk_data_timeout = connection_options[:chunk_data_timeout]
|
|
102
|
+
@persistent_data_timeout = http1_options[:persistent_data_timeout]
|
|
96
103
|
|
|
97
104
|
@selector = NIO::Selector.new
|
|
98
105
|
@queue = Queue.new
|
|
@@ -354,11 +361,11 @@ module Raptor
|
|
|
354
361
|
state = @socket_to_state[socket]
|
|
355
362
|
client = TimeoutClient.new(state)
|
|
356
363
|
timeout = if state[:persisted]
|
|
357
|
-
@
|
|
364
|
+
@persistent_data_timeout
|
|
358
365
|
elsif first_data_received?(state)
|
|
359
|
-
@
|
|
366
|
+
@chunk_data_timeout
|
|
360
367
|
else
|
|
361
|
-
@
|
|
368
|
+
@first_data_timeout
|
|
362
369
|
end
|
|
363
370
|
client.timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
364
371
|
@timeouts << client
|
data/lib/raptor/server.rb
CHANGED
|
@@ -19,9 +19,10 @@ module Raptor
|
|
|
19
19
|
#
|
|
20
20
|
# @example
|
|
21
21
|
# binder = Binder.new(["tcp://0.0.0.0:3000"])
|
|
22
|
-
# reactor = Reactor.new(ractor_pool, thread_pool,
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# reactor = Reactor.new(ractor_pool, thread_pool, connection_options: {}, http1_options: {})
|
|
23
|
+
# http1 = Http1.new(app, 3000)
|
|
24
|
+
# http2 = Http2.new(app, 3000)
|
|
25
|
+
# server = Server.new(binder, reactor, thread_pool, http1, http2, connection_options: { first_data_timeout: 30 })
|
|
25
26
|
# server.run
|
|
26
27
|
# # ... later
|
|
27
28
|
# server.shutdown
|
|
@@ -40,8 +41,10 @@ module Raptor
|
|
|
40
41
|
# @rbs @binder: Binder
|
|
41
42
|
# @rbs @reactor: Reactor
|
|
42
43
|
# @rbs @thread_pool: AtomicThreadPool
|
|
43
|
-
# @rbs @
|
|
44
|
-
# @rbs @
|
|
44
|
+
# @rbs @http1: Http1
|
|
45
|
+
# @rbs @http2: Http2
|
|
46
|
+
# @rbs @first_data_timeout: Integer
|
|
47
|
+
# @rbs @drain_accept_queue: bool
|
|
45
48
|
# @rbs @running: AtomicBoolean
|
|
46
49
|
|
|
47
50
|
# Creates a new Server instance.
|
|
@@ -49,17 +52,21 @@ module Raptor
|
|
|
49
52
|
# @param binder [Binder] the binder managing listening sockets
|
|
50
53
|
# @param reactor [Reactor] the reactor for handling client connections
|
|
51
54
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
52
|
-
# @param
|
|
53
|
-
# @param
|
|
55
|
+
# @param http1 [Http1] the HTTP/1.1 handler
|
|
56
|
+
# @param http2 [Http2] the HTTP/2 handler (provides the initial SETTINGS frame)
|
|
57
|
+
# @param connection_options [Hash] per-connection timeout configuration, used to bound TLS handshakes
|
|
58
|
+
# @param drain_accept_queue [Boolean] whether to drain the kernel accept queue on shutdown
|
|
54
59
|
# @return [void]
|
|
55
60
|
#
|
|
56
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool,
|
|
57
|
-
def initialize(binder, reactor, thread_pool,
|
|
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)
|
|
58
63
|
@binder = binder
|
|
59
64
|
@reactor = reactor
|
|
60
65
|
@thread_pool = thread_pool
|
|
61
|
-
@
|
|
62
|
-
@
|
|
66
|
+
@http1 = http1
|
|
67
|
+
@http2 = http2
|
|
68
|
+
@first_data_timeout = connection_options[:first_data_timeout]
|
|
69
|
+
@drain_accept_queue = drain_accept_queue
|
|
63
70
|
@running = AtomicBoolean.new(true)
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -74,12 +81,12 @@ module Raptor
|
|
|
74
81
|
#
|
|
75
82
|
# @rbs () -> Thread
|
|
76
83
|
def run
|
|
77
|
-
Thread.new
|
|
84
|
+
Thread.new do
|
|
78
85
|
Thread.current.name = "Server"
|
|
79
86
|
|
|
80
|
-
while running.true?
|
|
87
|
+
while @running.true?
|
|
81
88
|
begin
|
|
82
|
-
ready_servers, _, _ = IO.select(
|
|
89
|
+
ready_servers, _, _ = IO.select(@binder.listeners, nil, nil, 1)
|
|
83
90
|
rescue IOError, Errno::EBADF
|
|
84
91
|
break
|
|
85
92
|
end
|
|
@@ -87,28 +94,42 @@ module Raptor
|
|
|
87
94
|
next unless ready_servers
|
|
88
95
|
next if @reactor.backlog >= [(@thread_pool.size * 1.2).ceil, MIN_BACKPRESSURE_THRESHOLD].max
|
|
89
96
|
|
|
90
|
-
ready_servers.each
|
|
91
|
-
accept_connection(listener, reactor)
|
|
92
|
-
end
|
|
97
|
+
ready_servers.each { |listener| accept_connection(listener) }
|
|
93
98
|
end
|
|
94
99
|
end
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
# Gracefully shuts down the server.
|
|
98
103
|
#
|
|
99
|
-
# Stops accepting new connections and closes all listening sockets.
|
|
100
|
-
#
|
|
104
|
+
# Stops accepting new connections and closes all listening sockets. When
|
|
105
|
+
# `drain_accept_queue` is enabled, dispatches every connection already in
|
|
106
|
+
# the kernel accept queue before closing the listeners.
|
|
101
107
|
#
|
|
102
108
|
# @return [void]
|
|
103
109
|
#
|
|
104
110
|
# @rbs () -> void
|
|
105
111
|
def shutdown
|
|
106
112
|
@running.make_false
|
|
113
|
+
drain_accept_queue if @drain_accept_queue
|
|
107
114
|
@binder.close
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
private
|
|
111
118
|
|
|
119
|
+
# Dispatches every connection already in the kernel accept queue for each
|
|
120
|
+
# listener until all are drained.
|
|
121
|
+
#
|
|
122
|
+
# @return [void]
|
|
123
|
+
#
|
|
124
|
+
# @rbs () -> void
|
|
125
|
+
def drain_accept_queue
|
|
126
|
+
loop do
|
|
127
|
+
accepted = false
|
|
128
|
+
@binder.listeners.each { |listener| accepted = true if accept_connection(listener) }
|
|
129
|
+
break unless accepted
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
112
133
|
# Accepts a connection from the given listener and dispatches it.
|
|
113
134
|
#
|
|
114
135
|
# For SSL listeners the TLS handshake is offloaded to the thread pool so
|
|
@@ -118,15 +139,14 @@ module Raptor
|
|
|
118
139
|
# follow the HTTP/1.1 path.
|
|
119
140
|
#
|
|
120
141
|
# @param listener [TCPServer, UNIXServer, Binder::SslListener] the ready listener
|
|
121
|
-
# @
|
|
122
|
-
# @return [void]
|
|
142
|
+
# @return [Boolean] true if a connection was accepted, false if the listener had nothing to dispatch
|
|
123
143
|
#
|
|
124
|
-
# @rbs (TCPServer | UNIXServer | Binder::SslListener listener
|
|
125
|
-
def accept_connection(listener
|
|
144
|
+
# @rbs (TCPServer | UNIXServer | Binder::SslListener listener) -> bool
|
|
145
|
+
def accept_connection(listener)
|
|
126
146
|
tcp_client = begin
|
|
127
147
|
listener.is_a?(Binder::SslListener) ? listener.tcp_server.accept_nonblock : listener.accept_nonblock
|
|
128
148
|
rescue IO::WaitReadable
|
|
129
|
-
return
|
|
149
|
+
return false
|
|
130
150
|
end
|
|
131
151
|
|
|
132
152
|
if tcp_client.is_a?(TCPSocket)
|
|
@@ -138,19 +158,20 @@ module Raptor
|
|
|
138
158
|
|
|
139
159
|
if listener.is_a?(Binder::SslListener)
|
|
140
160
|
@thread_pool << proc do
|
|
141
|
-
dispatch_ssl_connection(listener, tcp_client, remote_addr
|
|
161
|
+
dispatch_ssl_connection(listener, tcp_client, remote_addr)
|
|
142
162
|
end
|
|
143
|
-
return
|
|
163
|
+
return true
|
|
144
164
|
end
|
|
145
165
|
|
|
146
|
-
@
|
|
166
|
+
@http1.eager_accept(
|
|
147
167
|
tcp_client,
|
|
148
168
|
tcp_client.object_id,
|
|
149
|
-
reactor,
|
|
169
|
+
@reactor,
|
|
150
170
|
@thread_pool,
|
|
151
171
|
remote_addr,
|
|
152
172
|
HTTP_SCHEME
|
|
153
173
|
)
|
|
174
|
+
true
|
|
154
175
|
end
|
|
155
176
|
|
|
156
177
|
# Performs the TLS handshake for an accepted SSL connection and dispatches
|
|
@@ -160,35 +181,34 @@ module Raptor
|
|
|
160
181
|
# @param listener [Binder::SslListener] the SSL listener that accepted the connection
|
|
161
182
|
# @param tcp_client [TCPSocket] the accepted TCP socket
|
|
162
183
|
# @param remote_addr [String] the client's IP address
|
|
163
|
-
# @param reactor [Reactor] the reactor to dispatch the connection to
|
|
164
184
|
# @return [void]
|
|
165
185
|
#
|
|
166
|
-
# @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr
|
|
167
|
-
def dispatch_ssl_connection(listener, tcp_client, remote_addr
|
|
186
|
+
# @rbs (Binder::SslListener listener, TCPSocket tcp_client, String remote_addr) -> void
|
|
187
|
+
def dispatch_ssl_connection(listener, tcp_client, remote_addr)
|
|
168
188
|
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, listener.ssl_context)
|
|
169
189
|
ssl_socket.sync_close = true
|
|
170
190
|
return unless perform_ssl_handshake(ssl_socket)
|
|
171
191
|
|
|
172
192
|
if ssl_socket.alpn_protocol == H2_PROTOCOL
|
|
173
|
-
ssl_socket.write(
|
|
193
|
+
ssl_socket.write(@http2.initial_settings_frame) rescue nil
|
|
174
194
|
|
|
175
|
-
reactor.add(
|
|
195
|
+
@reactor.add(
|
|
176
196
|
id: ssl_socket.object_id,
|
|
177
197
|
socket: ssl_socket,
|
|
178
198
|
remote_addr: remote_addr,
|
|
179
199
|
url_scheme: HTTPS_SCHEME,
|
|
180
200
|
protocol: :http2,
|
|
181
|
-
writer:
|
|
201
|
+
writer: @http2.create_writer,
|
|
182
202
|
flow_control: Http2::FlowControl.new
|
|
183
203
|
)
|
|
184
204
|
|
|
185
205
|
return
|
|
186
206
|
end
|
|
187
207
|
|
|
188
|
-
@
|
|
208
|
+
@http1.eager_accept(
|
|
189
209
|
ssl_socket,
|
|
190
210
|
ssl_socket.object_id,
|
|
191
|
-
reactor,
|
|
211
|
+
@reactor,
|
|
192
212
|
@thread_pool,
|
|
193
213
|
remote_addr,
|
|
194
214
|
HTTPS_SCHEME
|
|
@@ -204,7 +224,7 @@ module Raptor
|
|
|
204
224
|
#
|
|
205
225
|
# @rbs (OpenSSL::SSL::SSLSocket ssl_socket) -> bool
|
|
206
226
|
def perform_ssl_handshake(ssl_socket)
|
|
207
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @
|
|
227
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @first_data_timeout
|
|
208
228
|
|
|
209
229
|
begin
|
|
210
230
|
ssl_socket.accept_nonblock
|
data/lib/raptor/stats.rb
CHANGED
|
@@ -60,7 +60,7 @@ module Raptor
|
|
|
60
60
|
# @rbs (Integer index, pid: Integer, phase: Integer, requests: Integer, backlog: Integer, busy_threads: Integer, thread_capacity: Integer, started_at: Float, last_checkin: Float, booted: bool) -> void
|
|
61
61
|
def write(index, pid:, phase:, requests:, backlog:, busy_threads:, thread_capacity:, started_at:, last_checkin:, booted:)
|
|
62
62
|
data = [pid, index, phase, requests, backlog, busy_threads, thread_capacity, started_at, last_checkin, booted ? 1 : 0].pack(SLOT_FORMAT)
|
|
63
|
-
@mmap
|
|
63
|
+
@mmap[index * SLOT_SIZE, SLOT_SIZE] = data
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Returns stats for all worker slots.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Raptor
|
|
7
|
+
# Integration with systemd's service notification protocol and
|
|
8
|
+
# socket-activation file descriptors.
|
|
9
|
+
#
|
|
10
|
+
module Systemd
|
|
11
|
+
LISTEN_FDS_START = 3
|
|
12
|
+
|
|
13
|
+
LISTEN_FDNAMES_ENV = "LISTEN_FDNAMES"
|
|
14
|
+
LISTEN_FDS_ENV = "LISTEN_FDS"
|
|
15
|
+
LISTEN_PID_ENV = "LISTEN_PID"
|
|
16
|
+
NOTIFY_SOCKET_ENV = "NOTIFY_SOCKET"
|
|
17
|
+
|
|
18
|
+
# Sends `message` to the systemd notification socket, returning true on
|
|
19
|
+
# success and false when the socket is unset or the send fails.
|
|
20
|
+
#
|
|
21
|
+
# @param message [String] notify protocol payload, e.g. "READY=1"
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
#
|
|
24
|
+
# @rbs (String message) -> bool
|
|
25
|
+
def self.notify(message)
|
|
26
|
+
socket_path = ENV[NOTIFY_SOCKET_ENV]
|
|
27
|
+
return false if socket_path.nil? || socket_path.empty?
|
|
28
|
+
|
|
29
|
+
address = if socket_path.start_with?("@")
|
|
30
|
+
Socket.pack_sockaddr_un("\0#{socket_path[1..]}")
|
|
31
|
+
else
|
|
32
|
+
Socket.pack_sockaddr_un(socket_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
|
|
36
|
+
socket.send(message, 0, address)
|
|
37
|
+
true
|
|
38
|
+
rescue SystemCallError, IOError
|
|
39
|
+
false
|
|
40
|
+
ensure
|
|
41
|
+
socket&.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the file descriptors passed in via socket activation, or an
|
|
45
|
+
# empty array when systemd has not exported any.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Integer>]
|
|
48
|
+
#
|
|
49
|
+
# @rbs () -> Array[Integer]
|
|
50
|
+
def self.listen_fds
|
|
51
|
+
return [] unless ENV[LISTEN_PID_ENV]&.to_i == Process.pid
|
|
52
|
+
|
|
53
|
+
count = ENV[LISTEN_FDS_ENV]&.to_i || 0
|
|
54
|
+
Array.new(count) { |index| LISTEN_FDS_START + index }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Clears the socket-activation environment variables so children don't
|
|
58
|
+
# act on stale values.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
#
|
|
62
|
+
# @rbs () -> void
|
|
63
|
+
def self.clear_listen_env
|
|
64
|
+
ENV.delete(LISTEN_FDNAMES_ENV)
|
|
65
|
+
ENV.delete(LISTEN_FDS_ENV)
|
|
66
|
+
ENV.delete(LISTEN_PID_ENV)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/raptor/version.rb
CHANGED