raptor 0.6.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 +23 -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} +202 -81
- data/lib/raptor/http2.rb +149 -61
- 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 +64 -14
- 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,16 +218,20 @@ 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
|
|
225
230
|
|
|
231
|
+
EAGER_READ_TIMEOUT = 0.001
|
|
232
|
+
EAGER_READ_BUFFER_SIZE = 64 * 1024
|
|
233
|
+
EAGER_MAX_ROUNDS = 4
|
|
234
|
+
|
|
226
235
|
FLAG_END_STREAM = 0x1
|
|
227
236
|
FLAG_END_HEADERS = 0x4
|
|
228
237
|
FLAG_ACK = 0x1
|
|
@@ -236,38 +245,55 @@ module Raptor
|
|
|
236
245
|
|
|
237
246
|
SERVER_PROTOCOL = "HTTP/2"
|
|
238
247
|
RACK_HEADER_PREFIX = "rack."
|
|
239
|
-
HOP_BY_HOP_HEADERS =
|
|
248
|
+
HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
|
|
240
249
|
|
|
241
250
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
242
251
|
# @rbs @server_port: Integer
|
|
252
|
+
# @rbs @write_timeout: Integer
|
|
253
|
+
# @rbs @access_log_io: IO?
|
|
243
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
|
|
244
261
|
|
|
245
262
|
# Creates a new Http2 handler.
|
|
246
263
|
#
|
|
247
264
|
# @param app [#call] the Rack application to dispatch requests to
|
|
248
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
|
|
249
271
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
250
272
|
# @return [void]
|
|
251
273
|
#
|
|
252
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
253
|
-
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)
|
|
254
276
|
@app = app
|
|
255
277
|
@server_port = server_port
|
|
278
|
+
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
279
|
+
@access_log_io = access_log_io
|
|
256
280
|
@on_error = on_error
|
|
257
|
-
end
|
|
258
281
|
|
|
259
|
-
# Builds the initial server SETTINGS frame to send on connection establishment.
|
|
260
|
-
#
|
|
261
|
-
# @return [String] the encoded SETTINGS frame
|
|
262
|
-
#
|
|
263
|
-
# @rbs () -> String
|
|
264
|
-
def self.build_server_settings_frame
|
|
265
282
|
parser = Http2Parser.new
|
|
266
283
|
settings_payload = parser.build_settings(
|
|
267
|
-
max_concurrent_streams:
|
|
284
|
+
max_concurrent_streams: http2_options[:max_concurrent_streams],
|
|
268
285
|
initial_window_size: DEFAULT_WINDOW_SIZE
|
|
269
286
|
)
|
|
270
|
-
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)
|
|
271
297
|
end
|
|
272
298
|
|
|
273
299
|
# Processes HTTP/2 frames from the connection buffer.
|
|
@@ -500,7 +526,9 @@ module Raptor
|
|
|
500
526
|
# Handles a parsed HTTP/2 request from the ractor pool.
|
|
501
527
|
#
|
|
502
528
|
# Writes outgoing protocol frames to the socket, updates reactor state,
|
|
503
|
-
# and dispatches completed stream requests to the thread pool.
|
|
529
|
+
# and dispatches completed stream requests to the thread pool. Eagerly
|
|
530
|
+
# consumes subsequent frame batches that are already buffered, skipping
|
|
531
|
+
# the reactor and ractor pool hops while the connection is hot.
|
|
504
532
|
#
|
|
505
533
|
# @param result [Hash] the parsed result from the ractor pool
|
|
506
534
|
# @param reactor [Reactor] the reactor managing the connection
|
|
@@ -515,31 +543,42 @@ module Raptor
|
|
|
515
543
|
writer = reactor.writer_for(result[:id])
|
|
516
544
|
flow_control = reactor.flow_control_for(result[:id])
|
|
517
545
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
546
|
+
rounds = 0
|
|
547
|
+
loop do
|
|
548
|
+
if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
|
|
549
|
+
apply_flow_control_updates(flow_control, result)
|
|
550
|
+
end
|
|
521
551
|
|
|
522
|
-
|
|
552
|
+
writer.write_frames(socket, result[:outgoing_frames])
|
|
523
553
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
reactor.update_http2_state(result)
|
|
554
|
+
if result[:close_connection]
|
|
555
|
+
reactor.close_connection(result[:id])
|
|
556
|
+
return
|
|
557
|
+
end
|
|
530
558
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
559
|
+
result[:completed_requests]&.each do |request|
|
|
560
|
+
stream_id = request[:stream_id]
|
|
561
|
+
remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
534
562
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
563
|
+
thread_pool << proc do
|
|
564
|
+
dispatch_stream_request(
|
|
565
|
+
socket, writer, flow_control, stream_id,
|
|
566
|
+
request[:headers], request[:body],
|
|
567
|
+
remote_addr: remote_addr
|
|
568
|
+
)
|
|
569
|
+
end
|
|
541
570
|
end
|
|
571
|
+
|
|
572
|
+
rounds += 1
|
|
573
|
+
break if rounds >= EAGER_MAX_ROUNDS
|
|
574
|
+
|
|
575
|
+
next_batch = eager_read_next_batch(socket)
|
|
576
|
+
break unless next_batch
|
|
577
|
+
|
|
578
|
+
result = Raptor::Http2.process_frames(result.merge(buffer: result[:buffer] + next_batch))
|
|
542
579
|
end
|
|
580
|
+
|
|
581
|
+
reactor.update_http2_state(result)
|
|
543
582
|
end
|
|
544
583
|
|
|
545
584
|
private
|
|
@@ -566,6 +605,32 @@ module Raptor
|
|
|
566
605
|
end
|
|
567
606
|
end
|
|
568
607
|
|
|
608
|
+
# Reads the next frame batch from `socket` within a short window, or
|
|
609
|
+
# returns nil if nothing arrives in time.
|
|
610
|
+
#
|
|
611
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
612
|
+
# @return [String, nil] the bytes read, or nil if nothing was available
|
|
613
|
+
#
|
|
614
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
615
|
+
def eager_read_next_batch(socket)
|
|
616
|
+
return unless socket.wait_readable(EAGER_READ_TIMEOUT)
|
|
617
|
+
|
|
618
|
+
data = begin
|
|
619
|
+
socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
|
|
620
|
+
rescue IO::WaitReadable, EOFError, IOError
|
|
621
|
+
return
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
buffer = String.new
|
|
625
|
+
buffer << data
|
|
626
|
+
|
|
627
|
+
while socket.pending > 0
|
|
628
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
buffer
|
|
632
|
+
end
|
|
633
|
+
|
|
569
634
|
# Dispatches a completed stream request to the Rack app and writes
|
|
570
635
|
# the response back as HTTP/2 frames.
|
|
571
636
|
#
|
|
@@ -583,7 +648,8 @@ module Raptor
|
|
|
583
648
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
584
649
|
status, response_headers, response_body = @app.call(env)
|
|
585
650
|
|
|
586
|
-
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
|
|
587
653
|
rescue => error
|
|
588
654
|
write_http2_error_response(socket, writer, stream_id)
|
|
589
655
|
|
|
@@ -609,9 +675,9 @@ module Raptor
|
|
|
609
675
|
# @param status [Integer] HTTP status code
|
|
610
676
|
# @param headers [Hash] response headers from the Rack application
|
|
611
677
|
# @param body [Object] response body responding to each
|
|
612
|
-
# @return [
|
|
678
|
+
# @return [String] the response body size in bytes
|
|
613
679
|
#
|
|
614
|
-
# @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
|
|
615
681
|
def write_http2_response(socket, writer, flow_control, stream_id, status, headers, body)
|
|
616
682
|
parser = Http2Parser.new
|
|
617
683
|
|
|
@@ -630,11 +696,16 @@ module Raptor
|
|
|
630
696
|
|
|
631
697
|
encoded_headers = parser.encode_headers(header_pairs)
|
|
632
698
|
body_chunks = []
|
|
633
|
-
|
|
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
|
|
634
705
|
|
|
635
706
|
if body_chunks.empty?
|
|
636
707
|
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)])
|
|
637
|
-
return
|
|
708
|
+
return "0"
|
|
638
709
|
end
|
|
639
710
|
|
|
640
711
|
frames = [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)]
|
|
@@ -654,6 +725,7 @@ module Raptor
|
|
|
654
725
|
end
|
|
655
726
|
|
|
656
727
|
writer.write_frames(socket, frames)
|
|
728
|
+
body_bytes.to_s
|
|
657
729
|
end
|
|
658
730
|
|
|
659
731
|
# Writes a 500 error response as HTTP/2 frames.
|
|
@@ -674,6 +746,20 @@ module Raptor
|
|
|
674
746
|
)
|
|
675
747
|
end
|
|
676
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
|
+
|
|
677
763
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
|
678
764
|
#
|
|
679
765
|
# Translates HTTP/2 pseudo-headers into Rack-compatible environment keys
|
|
@@ -700,9 +786,9 @@ module Raptor
|
|
|
700
786
|
when ":authority" then env[Rack::HTTP_HOST] = value
|
|
701
787
|
end
|
|
702
788
|
elsif name == "content-type"
|
|
703
|
-
env[
|
|
789
|
+
env[Http::CONTENT_TYPE] = value
|
|
704
790
|
elsif name == "content-length"
|
|
705
|
-
env[
|
|
791
|
+
env[Http::CONTENT_LENGTH] = value
|
|
706
792
|
else
|
|
707
793
|
rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
708
794
|
env[rack_key] = value
|
|
@@ -720,11 +806,13 @@ module Raptor
|
|
|
720
806
|
env[Rack::PATH_INFO] = "" unless env.key?(Rack::PATH_INFO)
|
|
721
807
|
env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
|
|
722
808
|
|
|
723
|
-
if body.bytesize.positive? && !env.key?(
|
|
724
|
-
env[
|
|
809
|
+
if body.bytesize.positive? && !env.key?(Http::CONTENT_LENGTH)
|
|
810
|
+
env[Http::CONTENT_LENGTH] = body.bytesize.to_s
|
|
725
811
|
end
|
|
726
812
|
|
|
727
|
-
env[
|
|
813
|
+
env[Http::REMOTE_ADDR] = remote_addr
|
|
814
|
+
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
815
|
+
env[Http::HTTP_VERSION] = SERVER_PROTOCOL
|
|
728
816
|
|
|
729
817
|
populate_server_name_and_port(env)
|
|
730
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.
|