raptor 0.1.0 → 0.2.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 +6 -0
- data/README.md +13 -5
- data/lib/rackup/handler/raptor.rb +90 -0
- data/lib/raptor/cluster.rb +3 -2
- data/lib/raptor/http2.rb +87 -35
- data/lib/raptor/reactor.rb +12 -12
- data/lib/raptor/request.rb +113 -29
- data/lib/raptor/server.rb +22 -13
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/rackup/handler/raptor.rbs +40 -0
- data/sig/generated/raptor/cluster.rbs +1 -0
- data/sig/generated/raptor/http2.rbs +34 -9
- data/sig/generated/raptor/reactor.rbs +7 -5
- data/sig/generated/raptor/request.rbs +37 -5
- data/sig/generated/raptor/server.rbs +13 -7
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8391f9399db2e88dab106ad995a34060e6ae07f34c84825bfdcb066cd129dad4
|
|
4
|
+
data.tar.gz: 25cb3658e2aded284f302b84a1ee91d67344a5b5dd36c6c0e854381cfb4df048
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e398237ccb391aff663c4d75e5470c0e4e0e6f6b273e26bface4cd6793691c3297053ab0a5d744deac8fd087c4f7dec0beb8426d8a762d3a089902d11dbe53e5
|
|
7
|
+
data.tar.gz: c4cfd5f7ffdcf7f970cc8c85868629bac6c6061312120c0dc2a3c64fa6431cf475bc8fbb877dce0bad3febc2bd87ccc5342e2bc9c8dad196c69306431ae91345
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
- Add Rack handler for booting via `rackup` or `rails server`
|
|
6
|
+
- Replace the HTTP/2 per-connection write mutex with a lock-free writer
|
|
7
|
+
- Parse the first HTTP/1.1 request inline on the server thread
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2026-05-22
|
|
4
10
|
|
|
5
11
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Raptor
|
|
2
2
|
|
|
3
|
-
Raptor is a high-performance, multi-threaded, multi-process Ruby web server that leverages Ractors for parallel
|
|
4
|
-
and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK compression, and NIO for
|
|
3
|
+
Raptor is a high-performance, multi-threaded, multi-process Ruby Rack 3 web server that leverages Ractors for parallel
|
|
4
|
+
HTTP/1.1 and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK compression, and NIO for
|
|
5
|
+
non-blocking I/O.
|
|
5
6
|
|
|
6
7
|
## Installation
|
|
7
8
|
|
|
@@ -52,15 +53,22 @@ Raptor Cluster initializing:
|
|
|
52
53
|
Hello, World!%
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
Also works with `rackup` and `rails server`:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
> bundle exec rackup -s raptor hello_world.ru
|
|
60
|
+
> bundle exec rails server -u raptor
|
|
61
|
+
```
|
|
62
|
+
|
|
55
63
|
## Benchmarks
|
|
56
64
|
|
|
57
65
|
Raptor 0.1.0 vs Puma 8.0.1:
|
|
58
66
|
|
|
59
67
|
| Protocol | Raptor | Puma |
|
|
60
68
|
| --------------------- | ------------ | ------------ |
|
|
61
|
-
| HTTP/1.1 |
|
|
62
|
-
| HTTP/1.1 (keep-alive) |
|
|
63
|
-
| HTTP/2 |
|
|
69
|
+
| HTTP/1.1 | 20.3k req/s | 20.8k req/s |
|
|
70
|
+
| HTTP/1.1 (keep-alive) | 60.9k req/s | 45.4k req/s |
|
|
71
|
+
| HTTP/2 | 22.9k req/s | N/A |
|
|
64
72
|
|
|
65
73
|
> Ruby 4.0.4 +YJIT, macOS Apple Silicon. 4 workers, 3 threads, 12 concurrent connections.
|
|
66
74
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "etc"
|
|
5
|
+
|
|
6
|
+
module Rackup
|
|
7
|
+
module Handler
|
|
8
|
+
# Rack handler for booting Raptor through Rackup, `rails server`, or any
|
|
9
|
+
# other host that follows the Rack handler protocol.
|
|
10
|
+
#
|
|
11
|
+
module Raptor
|
|
12
|
+
DEFAULT_OPTIONS = {
|
|
13
|
+
Host: "0.0.0.0",
|
|
14
|
+
Port: 9292
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Boots a Raptor cluster serving the given Rack application.
|
|
18
|
+
#
|
|
19
|
+
# @param app [#call] the Rack application to serve
|
|
20
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
21
|
+
# @yield [cluster] the cluster instance, before it starts running
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
24
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
|
|
25
|
+
def self.run(app, **options)
|
|
26
|
+
require_relative "../../raptor/cli"
|
|
27
|
+
require_relative "../../raptor/cluster"
|
|
28
|
+
|
|
29
|
+
cluster = ::Raptor::Cluster.new(build_cluster_options(app, options))
|
|
30
|
+
|
|
31
|
+
yield cluster if block_given?
|
|
32
|
+
|
|
33
|
+
cluster.run
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the handler-specific options surfaced by `rackup --help`.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{String => String}] option spec to description mapping
|
|
39
|
+
#
|
|
40
|
+
# @rbs () -> Hash[String, String]
|
|
41
|
+
def self.valid_options
|
|
42
|
+
{
|
|
43
|
+
"Host=HOST" => "Hostname to listen on (default: #{DEFAULT_OPTIONS[:Host]})",
|
|
44
|
+
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
|
+
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
46
|
+
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
|
+
"Workers=NUM" => "Number of worker processes (default: nprocessors)"
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds a Raptor cluster options hash from Rack handler options.
|
|
52
|
+
#
|
|
53
|
+
# Options not explicitly supplied by the user (per the `:user_supplied_options`
|
|
54
|
+
# key) are treated as host-provided defaults.
|
|
55
|
+
#
|
|
56
|
+
# @param app [#call] the Rack application to serve
|
|
57
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
58
|
+
# @return [Hash{Symbol => untyped}] cluster configuration
|
|
59
|
+
#
|
|
60
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
61
|
+
def self.build_cluster_options(app, options)
|
|
62
|
+
defaults = DEFAULT_OPTIONS.dup
|
|
63
|
+
|
|
64
|
+
if user_supplied_options = options.delete(:user_supplied_options)
|
|
65
|
+
(options.keys - user_supplied_options).each do |key|
|
|
66
|
+
defaults[key] = options.delete(key)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
host = options[:Host] || defaults[:Host]
|
|
71
|
+
port = options[:Port] || defaults[:Port]
|
|
72
|
+
|
|
73
|
+
cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
app: app,
|
|
77
|
+
binds: ["tcp://#{host}:#{port}"],
|
|
78
|
+
threads: (options[:Threads] || cli_defaults[:threads]).to_i,
|
|
79
|
+
ractors: (options[:Ractors] || cli_defaults[:ractors]).to_i,
|
|
80
|
+
workers: (options[:Workers] || Etc.nprocessors).to_i,
|
|
81
|
+
client: cli_defaults[:client],
|
|
82
|
+
stats_file: nil
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
private_class_method :build_cluster_options
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
register :raptor, Raptor
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -77,6 +77,7 @@ module Raptor
|
|
|
77
77
|
# @option options [Integer] :ractors number of ractors per worker process
|
|
78
78
|
# @option options [Integer] :workers number of worker processes
|
|
79
79
|
# @option options [Array<String>] :binds array of bind URIs
|
|
80
|
+
# @option options [#call] :app pre-built Rack application
|
|
80
81
|
# @option options [String] :rackup path to Rack configuration file
|
|
81
82
|
# @option options [Hash] :client client timeout configuration
|
|
82
83
|
# @return [void]
|
|
@@ -90,7 +91,7 @@ module Raptor
|
|
|
90
91
|
|
|
91
92
|
@binder = Binder.new(options[:binds])
|
|
92
93
|
@server_port = @binder.server_port
|
|
93
|
-
@app = Rack::Builder.parse_file(options[:rackup])
|
|
94
|
+
@app = options[:app] || Rack::Builder.parse_file(options[:rackup])
|
|
94
95
|
log_initialization
|
|
95
96
|
|
|
96
97
|
@shutdown = false
|
|
@@ -233,7 +234,7 @@ module Raptor
|
|
|
233
234
|
reactor = Reactor.new(thread_pool, ractor_pool, client_options: @client_options)
|
|
234
235
|
reactor_thread = reactor.run
|
|
235
236
|
|
|
236
|
-
server = Server.new(@binder, reactor, thread_pool)
|
|
237
|
+
server = Server.new(@binder, reactor, thread_pool, request)
|
|
237
238
|
server_thread = server.run
|
|
238
239
|
|
|
239
240
|
puts "[#{Process.pid}] Worker #{index} booted"
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
|
+
require "atomic-ruby/atom"
|
|
6
7
|
require "rack"
|
|
7
8
|
|
|
8
9
|
require_relative "raptor_http2"
|
|
@@ -16,6 +17,63 @@ module Raptor
|
|
|
16
17
|
# pipeline used by HTTP/1.1 connections.
|
|
17
18
|
#
|
|
18
19
|
class Http2
|
|
20
|
+
# Lock-free per-connection frame writer.
|
|
21
|
+
#
|
|
22
|
+
# Serializes concurrent socket writes from multiple stream workers
|
|
23
|
+
# without blocking any of them.
|
|
24
|
+
#
|
|
25
|
+
class Writer
|
|
26
|
+
IDLE = :idle
|
|
27
|
+
|
|
28
|
+
# @rbs @state: Atom
|
|
29
|
+
|
|
30
|
+
# Creates a new Writer.
|
|
31
|
+
#
|
|
32
|
+
# @rbs () -> void
|
|
33
|
+
def initialize
|
|
34
|
+
@state = Atom.new(IDLE)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Writes frames to the socket, coordinating with concurrent writers
|
|
38
|
+
# so that exactly one thread is actively writing at any time.
|
|
39
|
+
#
|
|
40
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
41
|
+
# @param frames [Array<String>] frame bytes to write in order
|
|
42
|
+
# @return [void]
|
|
43
|
+
#
|
|
44
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
|
|
45
|
+
def write_frames(socket, frames)
|
|
46
|
+
return if frames.nil? || frames.empty?
|
|
47
|
+
|
|
48
|
+
claimed = false
|
|
49
|
+
@state.swap do |current|
|
|
50
|
+
if current.equal?(IDLE)
|
|
51
|
+
claimed = true
|
|
52
|
+
frames
|
|
53
|
+
else
|
|
54
|
+
claimed = false
|
|
55
|
+
current + frames
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return unless claimed
|
|
60
|
+
|
|
61
|
+
loop do
|
|
62
|
+
pending = nil
|
|
63
|
+
@state.swap do |current|
|
|
64
|
+
pending = current
|
|
65
|
+
current.empty? ? IDLE : []
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
break if pending.empty?
|
|
69
|
+
|
|
70
|
+
pending.each do |frame|
|
|
71
|
+
socket.write(frame) rescue nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
19
77
|
FLAG_END_STREAM = 0x1
|
|
20
78
|
FLAG_END_HEADERS = 0x4
|
|
21
79
|
FLAG_ACK = 0x1
|
|
@@ -215,13 +273,9 @@ module Raptor
|
|
|
215
273
|
socket = reactor.socket_for(result[:id])
|
|
216
274
|
return unless socket
|
|
217
275
|
|
|
218
|
-
|
|
276
|
+
writer = reactor.writer_for(result[:id])
|
|
219
277
|
|
|
220
|
-
|
|
221
|
-
mutex.synchronize do
|
|
222
|
-
result[:outgoing_frames].each { |frame| socket.write(frame) rescue nil }
|
|
223
|
-
end
|
|
224
|
-
end
|
|
278
|
+
writer.write_frames(socket, result[:outgoing_frames])
|
|
225
279
|
|
|
226
280
|
reactor.update_http2_state(result)
|
|
227
281
|
|
|
@@ -231,7 +285,7 @@ module Raptor
|
|
|
231
285
|
|
|
232
286
|
thread_pool << proc do
|
|
233
287
|
dispatch_stream_request(
|
|
234
|
-
socket,
|
|
288
|
+
socket, writer, stream_id,
|
|
235
289
|
request[:headers], request[:body],
|
|
236
290
|
remote_addr: remote_addr
|
|
237
291
|
)
|
|
@@ -245,21 +299,21 @@ module Raptor
|
|
|
245
299
|
# the response back as HTTP/2 frames.
|
|
246
300
|
#
|
|
247
301
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
248
|
-
# @param
|
|
302
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
249
303
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
250
304
|
# @param headers [Array<Array(String, String)>] request headers
|
|
251
305
|
# @param body [String] request body
|
|
252
306
|
# @param remote_addr [String] the client IP address
|
|
253
307
|
# @return [void]
|
|
254
308
|
#
|
|
255
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
256
|
-
def dispatch_stream_request(socket,
|
|
309
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
310
|
+
def dispatch_stream_request(socket, writer, stream_id, headers, body, remote_addr:)
|
|
257
311
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
258
312
|
status, response_headers, response_body = @app.call(env)
|
|
259
313
|
|
|
260
|
-
write_http2_response(socket,
|
|
314
|
+
write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
|
|
261
315
|
rescue
|
|
262
|
-
write_http2_error_response(socket,
|
|
316
|
+
write_http2_error_response(socket, writer, stream_id)
|
|
263
317
|
raise
|
|
264
318
|
ensure
|
|
265
319
|
response_body.close if response_body.respond_to?(:close)
|
|
@@ -268,15 +322,15 @@ module Raptor
|
|
|
268
322
|
# Writes a Rack response as HTTP/2 frames to the socket.
|
|
269
323
|
#
|
|
270
324
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
271
|
-
# @param
|
|
325
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
272
326
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
273
327
|
# @param status [Integer] HTTP status code
|
|
274
328
|
# @param headers [Hash] response headers from the Rack application
|
|
275
329
|
# @param body [Object] response body responding to each
|
|
276
330
|
# @return [void]
|
|
277
331
|
#
|
|
278
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
279
|
-
def write_http2_response(socket,
|
|
332
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
333
|
+
def write_http2_response(socket, writer, stream_id, status, headers, body)
|
|
280
334
|
parser = Http2Parser.new
|
|
281
335
|
|
|
282
336
|
header_pairs = [[":status", status.to_s]]
|
|
@@ -296,40 +350,38 @@ module Raptor
|
|
|
296
350
|
body_chunks = []
|
|
297
351
|
body.each { |chunk| body_chunks << chunk unless chunk.empty? }
|
|
298
352
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
353
|
+
frames = []
|
|
354
|
+
if body_chunks.empty?
|
|
355
|
+
frames << parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)
|
|
356
|
+
else
|
|
357
|
+
frames << parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)
|
|
304
358
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
end
|
|
359
|
+
last_index = body_chunks.size - 1
|
|
360
|
+
body_chunks.each_with_index do |chunk, index|
|
|
361
|
+
flags = index == last_index ? FLAG_END_STREAM : 0
|
|
362
|
+
frames << parser.build_frame(:data, flags, stream_id, chunk)
|
|
310
363
|
end
|
|
311
364
|
end
|
|
312
|
-
|
|
313
|
-
|
|
365
|
+
|
|
366
|
+
writer.write_frames(socket, frames)
|
|
314
367
|
end
|
|
315
368
|
|
|
316
369
|
# Writes a 500 error response as HTTP/2 frames.
|
|
317
370
|
#
|
|
318
371
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
319
|
-
# @param
|
|
372
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
320
373
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
321
374
|
# @return [void]
|
|
322
375
|
#
|
|
323
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
324
|
-
def write_http2_error_response(socket,
|
|
376
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
377
|
+
def write_http2_error_response(socket, writer, stream_id)
|
|
325
378
|
parser = Http2Parser.new
|
|
326
379
|
encoded = parser.encode_headers([[":status", "500"]])
|
|
327
380
|
|
|
328
|
-
|
|
329
|
-
socket
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# Connection closed
|
|
381
|
+
writer.write_frames(
|
|
382
|
+
socket,
|
|
383
|
+
[parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded)]
|
|
384
|
+
)
|
|
333
385
|
end
|
|
334
386
|
|
|
335
387
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
data/lib/raptor/reactor.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Raptor
|
|
|
75
75
|
# @rbs @id_to_socket: Hash[Integer, TCPSocket]
|
|
76
76
|
# @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
|
|
77
77
|
# @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
|
|
78
|
-
# @rbs @
|
|
78
|
+
# @rbs @id_to_writer: Hash[Integer, untyped]
|
|
79
79
|
|
|
80
80
|
# Creates a new Reactor instance.
|
|
81
81
|
#
|
|
@@ -100,7 +100,7 @@ module Raptor
|
|
|
100
100
|
@id_to_socket = {}
|
|
101
101
|
@socket_to_state = {}
|
|
102
102
|
@id_to_timeout = {}
|
|
103
|
-
@
|
|
103
|
+
@id_to_writer = {}
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Starts the reactor's main event loop in a new thread.
|
|
@@ -162,12 +162,10 @@ module Raptor
|
|
|
162
162
|
def add(state)
|
|
163
163
|
socket = state[:socket]
|
|
164
164
|
state.delete(:socket)
|
|
165
|
+
writer = state.delete(:writer)
|
|
165
166
|
@id_to_socket[state[:id]] = socket
|
|
166
167
|
@socket_to_state[socket] = state
|
|
167
|
-
|
|
168
|
-
if state[:protocol] == :http2
|
|
169
|
-
@id_to_mutex[state[:id]] = Mutex.new
|
|
170
|
-
end
|
|
168
|
+
@id_to_writer[state[:id]] = writer if writer
|
|
171
169
|
|
|
172
170
|
read_and_queue_for_parse(socket, state)
|
|
173
171
|
end
|
|
@@ -252,14 +250,16 @@ module Raptor
|
|
|
252
250
|
@id_to_socket[id]
|
|
253
251
|
end
|
|
254
252
|
|
|
255
|
-
# Returns the
|
|
253
|
+
# Returns the writer object associated with a given connection, if one
|
|
254
|
+
# was supplied when the connection was added. Used by protocol handlers
|
|
255
|
+
# that need to coordinate concurrent socket writes.
|
|
256
256
|
#
|
|
257
257
|
# @param id [Integer] unique client identifier
|
|
258
|
-
# @return [
|
|
258
|
+
# @return [Object, nil] the writer, if found
|
|
259
259
|
#
|
|
260
|
-
# @rbs (Integer id) ->
|
|
261
|
-
def
|
|
262
|
-
@
|
|
260
|
+
# @rbs (Integer id) -> untyped?
|
|
261
|
+
def writer_for(id)
|
|
262
|
+
@id_to_writer[id]
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
# Updates connection state for an HTTP/2 connection after frame processing.
|
|
@@ -384,7 +384,7 @@ module Raptor
|
|
|
384
384
|
def cleanup(socket)
|
|
385
385
|
state = @socket_to_state.delete(socket)
|
|
386
386
|
@id_to_socket.delete(state[:id])
|
|
387
|
-
@
|
|
387
|
+
@id_to_writer.delete(state[:id])
|
|
388
388
|
socket.close
|
|
389
389
|
end
|
|
390
390
|
|
data/lib/raptor/request.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Raptor
|
|
|
20
20
|
class Request
|
|
21
21
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
22
22
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
23
|
-
|
|
23
|
+
READ_BUFFER_SIZE = 64 * 1024
|
|
24
24
|
WRITE_TIMEOUT = 5
|
|
25
25
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
26
26
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
@@ -59,6 +59,35 @@ module Raptor
|
|
|
59
59
|
def message = "could not write response"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Decodes a chunked transfer-encoded body buffer.
|
|
63
|
+
#
|
|
64
|
+
# Returns the decoded bytes and a flag indicating whether the terminating
|
|
65
|
+
# zero-length chunk was found. The decoder stops at the first unparseable
|
|
66
|
+
# boundary (incomplete CRLF) or zero-length chunk.
|
|
67
|
+
#
|
|
68
|
+
# @param buffer [String] the raw body buffer to decode
|
|
69
|
+
# @return [Array(String, Boolean)] decoded body and completion flag
|
|
70
|
+
#
|
|
71
|
+
# @rbs (String buffer) -> [String, bool]
|
|
72
|
+
def self.decode_chunked(buffer)
|
|
73
|
+
decoded = String.new
|
|
74
|
+
offset = 0
|
|
75
|
+
|
|
76
|
+
while offset < buffer.bytesize
|
|
77
|
+
crlf = buffer.index("\r\n", offset)
|
|
78
|
+
return [decoded, false] unless crlf
|
|
79
|
+
|
|
80
|
+
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
81
|
+
return [decoded, true] if chunk_size == 0
|
|
82
|
+
|
|
83
|
+
offset = crlf + 2
|
|
84
|
+
decoded << buffer.byteslice(offset, chunk_size)
|
|
85
|
+
offset += chunk_size + 2
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
[decoded, false]
|
|
89
|
+
end
|
|
90
|
+
|
|
62
91
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
63
92
|
# @rbs @server_port: Integer
|
|
64
93
|
|
|
@@ -74,6 +103,73 @@ module Raptor
|
|
|
74
103
|
@server_port = server_port
|
|
75
104
|
end
|
|
76
105
|
|
|
106
|
+
# Eagerly reads and parses the first request on a freshly accepted
|
|
107
|
+
# connection on the server thread, dispatching directly to the thread pool
|
|
108
|
+
# when complete. Falls back to the reactor when more data is needed.
|
|
109
|
+
#
|
|
110
|
+
# @param socket [TCPSocket] the freshly accepted client socket
|
|
111
|
+
# @param id [Integer] unique client identifier
|
|
112
|
+
# @param reactor [Reactor] the reactor for fallback registration
|
|
113
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
114
|
+
# @param remote_addr [String] client IP address
|
|
115
|
+
# @param url_scheme [String] "http" or "https"
|
|
116
|
+
# @return [void]
|
|
117
|
+
#
|
|
118
|
+
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
119
|
+
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
120
|
+
data = begin
|
|
121
|
+
socket.read_nonblock(READ_BUFFER_SIZE)
|
|
122
|
+
rescue IO::WaitReadable
|
|
123
|
+
reactor.add(
|
|
124
|
+
id: id,
|
|
125
|
+
socket: socket,
|
|
126
|
+
remote_addr: remote_addr,
|
|
127
|
+
url_scheme: url_scheme
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
rescue EOFError, IOError
|
|
131
|
+
socket.close rescue nil
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
buffer = String.new
|
|
136
|
+
buffer << data
|
|
137
|
+
|
|
138
|
+
while socket.respond_to?(:pending) && socket.pending > 0
|
|
139
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
parser = HttpParser.new
|
|
143
|
+
env = {}
|
|
144
|
+
nread = parser.execute(env, buffer, 0)
|
|
145
|
+
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
146
|
+
|
|
147
|
+
body = nil
|
|
148
|
+
if !parser.finished?
|
|
149
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
150
|
+
return
|
|
151
|
+
elsif parser.has_body?
|
|
152
|
+
body = buffer.byteslice(nread..-1) || ""
|
|
153
|
+
|
|
154
|
+
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
155
|
+
body, chunked_complete = Request.decode_chunked(body)
|
|
156
|
+
if chunked_complete
|
|
157
|
+
env.delete(HTTP_TRANSFER_ENCODING)
|
|
158
|
+
else
|
|
159
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
elsif parser.content_length > body.bytesize
|
|
163
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
thread_pool << proc do
|
|
169
|
+
process_client(socket, id, env, parse_data, body, reactor, thread_pool, 1, remote_addr, url_scheme)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
77
173
|
# Returns a Proc for HTTP parsing work in Ractor context.
|
|
78
174
|
#
|
|
79
175
|
# The returned Proc processes raw socket data through the appropriate
|
|
@@ -102,25 +198,7 @@ module Raptor
|
|
|
102
198
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
103
199
|
|
|
104
200
|
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
105
|
-
decoded_body =
|
|
106
|
-
offset = 0
|
|
107
|
-
chunked_complete = false
|
|
108
|
-
|
|
109
|
-
while offset < body_buffer.bytesize
|
|
110
|
-
crlf = body_buffer.index("\r\n", offset)
|
|
111
|
-
break unless crlf
|
|
112
|
-
|
|
113
|
-
chunk_size = body_buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
114
|
-
|
|
115
|
-
if chunk_size == 0
|
|
116
|
-
chunked_complete = true
|
|
117
|
-
break
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
offset = crlf + 2
|
|
121
|
-
decoded_body << body_buffer.byteslice(offset, chunk_size)
|
|
122
|
-
offset += chunk_size + 2
|
|
123
|
-
end
|
|
201
|
+
decoded_body, chunked_complete = Raptor::Request.decode_chunked(body_buffer)
|
|
124
202
|
|
|
125
203
|
if chunked_complete
|
|
126
204
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
@@ -278,7 +356,7 @@ module Raptor
|
|
|
278
356
|
end
|
|
279
357
|
|
|
280
358
|
data = begin
|
|
281
|
-
socket.read_nonblock(
|
|
359
|
+
socket.read_nonblock(READ_BUFFER_SIZE)
|
|
282
360
|
rescue IO::WaitReadable
|
|
283
361
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
284
362
|
return
|
|
@@ -346,8 +424,12 @@ module Raptor
|
|
|
346
424
|
end
|
|
347
425
|
end
|
|
348
426
|
|
|
349
|
-
# Re-registers a socket with the reactor for further processing
|
|
350
|
-
#
|
|
427
|
+
# Re-registers a socket with the reactor for further processing when
|
|
428
|
+
# an incomplete request is received during eager accept or eager keep-alive.
|
|
429
|
+
#
|
|
430
|
+
# The persisted flag selects between persistent_data_timeout (for
|
|
431
|
+
# kept-alive connections awaiting the next request) and chunk_data_timeout
|
|
432
|
+
# (for fresh connections awaiting the rest of the first request).
|
|
351
433
|
#
|
|
352
434
|
# @param socket [TCPSocket] the client socket
|
|
353
435
|
# @param id [Integer] unique client identifier
|
|
@@ -358,21 +440,23 @@ module Raptor
|
|
|
358
440
|
# @param request_count [Integer] number of requests handled on this connection
|
|
359
441
|
# @param remote_addr [String] client IP address
|
|
360
442
|
# @param url_scheme [String] "http" or "https"
|
|
443
|
+
# @param persisted [Boolean] whether the connection has already completed at least one request
|
|
361
444
|
# @return [void]
|
|
362
445
|
#
|
|
363
|
-
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
364
|
-
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
446
|
+
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
447
|
+
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
|
|
365
448
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
366
|
-
|
|
449
|
+
state = {
|
|
367
450
|
id: id,
|
|
368
451
|
buffer: buffer,
|
|
369
452
|
env: env,
|
|
370
453
|
request_count: request_count,
|
|
371
454
|
parse_data: parse_data,
|
|
372
455
|
remote_addr: remote_addr,
|
|
373
|
-
url_scheme: url_scheme
|
|
374
|
-
|
|
375
|
-
|
|
456
|
+
url_scheme: url_scheme
|
|
457
|
+
}
|
|
458
|
+
state[:persisted] = true if persisted
|
|
459
|
+
reactor.update_state(Ractor.make_shareable(state))
|
|
376
460
|
end
|
|
377
461
|
|
|
378
462
|
# Builds a Rack environment hash from parsed HTTP request data.
|
data/lib/raptor/server.rb
CHANGED
|
@@ -15,16 +15,19 @@ module Raptor
|
|
|
15
15
|
#
|
|
16
16
|
# Supports TCP, Unix domain, and SSL listeners transparently. TCP_NODELAY is
|
|
17
17
|
# applied only to TCP sockets, and SSL handshakes are performed synchronously
|
|
18
|
-
# before
|
|
18
|
+
# before the connection is dispatched.
|
|
19
19
|
#
|
|
20
|
-
# For
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# For HTTP/1.1 connections the first request is parsed inline on the server
|
|
21
|
+
# thread and dispatched directly to the thread pool, falling back to the
|
|
22
|
+
# reactor only when more data is needed. For HTTP/2 connections (negotiated
|
|
23
|
+
# via ALPN) the server sends initial SETTINGS and registers the connection
|
|
24
|
+
# with the reactor for frame processing through the ractor pool.
|
|
23
25
|
#
|
|
24
26
|
# @example
|
|
25
27
|
# binder = Binder.new(["tcp://0.0.0.0:3000"])
|
|
26
28
|
# reactor = Reactor.new(thread_pool, ractor_pool, client_options: {})
|
|
27
|
-
#
|
|
29
|
+
# request = Request.new(app, 3000)
|
|
30
|
+
# server = Server.new(binder, reactor, thread_pool, request)
|
|
28
31
|
# server.run
|
|
29
32
|
# # ... later
|
|
30
33
|
# server.shutdown
|
|
@@ -37,6 +40,7 @@ module Raptor
|
|
|
37
40
|
# @rbs @binder: Binder
|
|
38
41
|
# @rbs @reactor: Reactor
|
|
39
42
|
# @rbs @thread_pool: AtomicThreadPool
|
|
43
|
+
# @rbs @request: Request
|
|
40
44
|
# @rbs @running: AtomicBoolean
|
|
41
45
|
|
|
42
46
|
# Creates a new Server instance.
|
|
@@ -44,13 +48,15 @@ module Raptor
|
|
|
44
48
|
# @param binder [Binder] the binder managing listening sockets
|
|
45
49
|
# @param reactor [Reactor] the reactor for handling client connections
|
|
46
50
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
51
|
+
# @param request [Request] the HTTP/1.1 request handler
|
|
47
52
|
# @return [void]
|
|
48
53
|
#
|
|
49
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
50
|
-
def initialize(binder, reactor, thread_pool)
|
|
54
|
+
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
|
|
55
|
+
def initialize(binder, reactor, thread_pool, request)
|
|
51
56
|
@binder = binder
|
|
52
57
|
@reactor = reactor
|
|
53
58
|
@thread_pool = thread_pool
|
|
59
|
+
@request = request
|
|
54
60
|
@running = AtomicBoolean.new(true)
|
|
55
61
|
end
|
|
56
62
|
|
|
@@ -149,18 +155,21 @@ module Raptor
|
|
|
149
155
|
socket: ssl_socket,
|
|
150
156
|
remote_addr: remote_addr,
|
|
151
157
|
url_scheme: HTTPS_SCHEME,
|
|
152
|
-
protocol: :http2
|
|
158
|
+
protocol: :http2,
|
|
159
|
+
writer: Http2::Writer.new
|
|
153
160
|
)
|
|
154
161
|
|
|
155
162
|
return
|
|
156
163
|
end
|
|
157
164
|
end
|
|
158
165
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
@request.eager_accept(
|
|
167
|
+
client,
|
|
168
|
+
client.object_id,
|
|
169
|
+
reactor,
|
|
170
|
+
@thread_pool,
|
|
171
|
+
remote_addr,
|
|
172
|
+
url_scheme
|
|
164
173
|
)
|
|
165
174
|
end
|
|
166
175
|
end
|
data/lib/raptor/version.rb
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Generated from lib/rackup/handler/raptor.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Rackup
|
|
4
|
+
module Handler
|
|
5
|
+
# Rack handler for booting Raptor through Rackup, `rails server`, or any
|
|
6
|
+
# other host that follows the Rack handler protocol.
|
|
7
|
+
module Raptor
|
|
8
|
+
DEFAULT_OPTIONS: untyped
|
|
9
|
+
|
|
10
|
+
# Boots a Raptor cluster serving the given Rack application.
|
|
11
|
+
#
|
|
12
|
+
# @param app [#call] the Rack application to serve
|
|
13
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
14
|
+
# @yield [cluster] the cluster instance, before it starts running
|
|
15
|
+
# @return [void]
|
|
16
|
+
#
|
|
17
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
|
|
18
|
+
def self.run: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, **untyped options) { (::Raptor::Cluster) -> void } -> void
|
|
19
|
+
|
|
20
|
+
# Returns the handler-specific options surfaced by `rackup --help`.
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash{String => String}] option spec to description mapping
|
|
23
|
+
#
|
|
24
|
+
# @rbs () -> Hash[String, String]
|
|
25
|
+
def self.valid_options: () -> Hash[String, String]
|
|
26
|
+
|
|
27
|
+
# Builds a Raptor cluster options hash from Rack handler options.
|
|
28
|
+
#
|
|
29
|
+
# Options not explicitly supplied by the user (per the `:user_supplied_options`
|
|
30
|
+
# key) are treated as host-provided defaults.
|
|
31
|
+
#
|
|
32
|
+
# @param app [#call] the Rack application to serve
|
|
33
|
+
# @param options [Hash] handler options provided by Rackup or the host
|
|
34
|
+
# @return [Hash{Symbol => untyped}] cluster configuration
|
|
35
|
+
#
|
|
36
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
37
|
+
def self.build_cluster_options: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -70,6 +70,7 @@ module Raptor
|
|
|
70
70
|
# @option options [Integer] :ractors number of ractors per worker process
|
|
71
71
|
# @option options [Integer] :workers number of worker processes
|
|
72
72
|
# @option options [Array<String>] :binds array of bind URIs
|
|
73
|
+
# @option options [#call] :app pre-built Rack application
|
|
73
74
|
# @option options [String] :rackup path to Rack configuration file
|
|
74
75
|
# @option options [Hash] :client client timeout configuration
|
|
75
76
|
# @return [void]
|
|
@@ -8,6 +8,31 @@ module Raptor
|
|
|
8
8
|
# It integrates with the same reactor, ractor pool, and thread pool
|
|
9
9
|
# pipeline used by HTTP/1.1 connections.
|
|
10
10
|
class Http2
|
|
11
|
+
# Lock-free per-connection frame writer.
|
|
12
|
+
#
|
|
13
|
+
# Serializes concurrent socket writes from multiple stream workers
|
|
14
|
+
# without blocking any of them.
|
|
15
|
+
class Writer
|
|
16
|
+
IDLE: ::Symbol
|
|
17
|
+
|
|
18
|
+
@state: Atom
|
|
19
|
+
|
|
20
|
+
# Creates a new Writer.
|
|
21
|
+
#
|
|
22
|
+
# @rbs () -> void
|
|
23
|
+
def initialize: () -> void
|
|
24
|
+
|
|
25
|
+
# Writes frames to the socket, coordinating with concurrent writers
|
|
26
|
+
# so that exactly one thread is actively writing at any time.
|
|
27
|
+
#
|
|
28
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
29
|
+
# @param frames [Array<String>] frame bytes to write in order
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
|
|
33
|
+
def write_frames: (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
|
|
34
|
+
end
|
|
35
|
+
|
|
11
36
|
FLAG_END_STREAM: ::Integer
|
|
12
37
|
|
|
13
38
|
FLAG_END_HEADERS: ::Integer
|
|
@@ -88,38 +113,38 @@ module Raptor
|
|
|
88
113
|
# the response back as HTTP/2 frames.
|
|
89
114
|
#
|
|
90
115
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
91
|
-
# @param
|
|
116
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
92
117
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
93
118
|
# @param headers [Array<Array(String, String)>] request headers
|
|
94
119
|
# @param body [String] request body
|
|
95
120
|
# @param remote_addr [String] the client IP address
|
|
96
121
|
# @return [void]
|
|
97
122
|
#
|
|
98
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
99
|
-
def dispatch_stream_request: (OpenSSL::SSL::SSLSocket socket,
|
|
123
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
124
|
+
def dispatch_stream_request: (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[ String, String ]] headers, String body, remote_addr: String) -> void
|
|
100
125
|
|
|
101
126
|
# Writes a Rack response as HTTP/2 frames to the socket.
|
|
102
127
|
#
|
|
103
128
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
104
|
-
# @param
|
|
129
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
105
130
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
106
131
|
# @param status [Integer] HTTP status code
|
|
107
132
|
# @param headers [Hash] response headers from the Rack application
|
|
108
133
|
# @param body [Object] response body responding to each
|
|
109
134
|
# @return [void]
|
|
110
135
|
#
|
|
111
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
112
|
-
def write_http2_response: (OpenSSL::SSL::SSLSocket socket,
|
|
136
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
137
|
+
def write_http2_response: (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
113
138
|
|
|
114
139
|
# Writes a 500 error response as HTTP/2 frames.
|
|
115
140
|
#
|
|
116
141
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
117
|
-
# @param
|
|
142
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
118
143
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
119
144
|
# @return [void]
|
|
120
145
|
#
|
|
121
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
122
|
-
def write_http2_error_response: (OpenSSL::SSL::SSLSocket socket,
|
|
146
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
147
|
+
def write_http2_error_response: (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
123
148
|
|
|
124
149
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
|
125
150
|
#
|
|
@@ -55,7 +55,7 @@ module Raptor
|
|
|
55
55
|
|
|
56
56
|
TIMEOUT_RESPONSE: ::String
|
|
57
57
|
|
|
58
|
-
@
|
|
58
|
+
@id_to_writer: Hash[Integer, untyped]
|
|
59
59
|
|
|
60
60
|
@id_to_timeout: Hash[Integer, TimeoutClient]
|
|
61
61
|
|
|
@@ -159,13 +159,15 @@ module Raptor
|
|
|
159
159
|
# @rbs (Integer id) -> TCPSocket?
|
|
160
160
|
def socket_for: (Integer id) -> TCPSocket?
|
|
161
161
|
|
|
162
|
-
# Returns the
|
|
162
|
+
# Returns the writer object associated with a given connection, if one
|
|
163
|
+
# was supplied when the connection was added. Used by protocol handlers
|
|
164
|
+
# that need to coordinate concurrent socket writes.
|
|
163
165
|
#
|
|
164
166
|
# @param id [Integer] unique client identifier
|
|
165
|
-
# @return [
|
|
167
|
+
# @return [Object, nil] the writer, if found
|
|
166
168
|
#
|
|
167
|
-
# @rbs (Integer id) ->
|
|
168
|
-
def
|
|
169
|
+
# @rbs (Integer id) -> untyped?
|
|
170
|
+
def writer_for: (Integer id) -> untyped?
|
|
169
171
|
|
|
170
172
|
# Updates connection state for an HTTP/2 connection after frame processing.
|
|
171
173
|
#
|
|
@@ -13,7 +13,7 @@ module Raptor
|
|
|
13
13
|
|
|
14
14
|
FILE_CHUNK_SIZE: untyped
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
READ_BUFFER_SIZE: untyped
|
|
17
17
|
|
|
18
18
|
WRITE_TIMEOUT: ::Integer
|
|
19
19
|
|
|
@@ -63,6 +63,18 @@ module Raptor
|
|
|
63
63
|
def message: () -> String
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Decodes a chunked transfer-encoded body buffer.
|
|
67
|
+
#
|
|
68
|
+
# Returns the decoded bytes and a flag indicating whether the terminating
|
|
69
|
+
# zero-length chunk was found. The decoder stops at the first unparseable
|
|
70
|
+
# boundary (incomplete CRLF) or zero-length chunk.
|
|
71
|
+
#
|
|
72
|
+
# @param buffer [String] the raw body buffer to decode
|
|
73
|
+
# @return [Array(String, Boolean)] decoded body and completion flag
|
|
74
|
+
#
|
|
75
|
+
# @rbs (String buffer) -> [String, bool]
|
|
76
|
+
def self.decode_chunked: (String buffer) -> [ String, bool ]
|
|
77
|
+
|
|
66
78
|
@server_port: Integer
|
|
67
79
|
|
|
68
80
|
@app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
|
|
@@ -76,6 +88,21 @@ module Raptor
|
|
|
76
88
|
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
77
89
|
def initialize: (^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ] app, Integer server_port) -> void
|
|
78
90
|
|
|
91
|
+
# Eagerly reads and parses the first request on a freshly accepted
|
|
92
|
+
# connection on the server thread, dispatching directly to the thread pool
|
|
93
|
+
# when complete. Falls back to the reactor when more data is needed.
|
|
94
|
+
#
|
|
95
|
+
# @param socket [TCPSocket] the freshly accepted client socket
|
|
96
|
+
# @param id [Integer] unique client identifier
|
|
97
|
+
# @param reactor [Reactor] the reactor for fallback registration
|
|
98
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
99
|
+
# @param remote_addr [String] client IP address
|
|
100
|
+
# @param url_scheme [String] "http" or "https"
|
|
101
|
+
# @return [void]
|
|
102
|
+
#
|
|
103
|
+
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
104
|
+
def eager_accept: (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
105
|
+
|
|
79
106
|
# Returns a Proc for HTTP parsing work in Ractor context.
|
|
80
107
|
#
|
|
81
108
|
# The returned Proc processes raw socket data through the appropriate
|
|
@@ -154,8 +181,12 @@ module Raptor
|
|
|
154
181
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
155
182
|
def eager_keepalive: (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
156
183
|
|
|
157
|
-
# Re-registers a socket with the reactor for further processing
|
|
158
|
-
#
|
|
184
|
+
# Re-registers a socket with the reactor for further processing when
|
|
185
|
+
# an incomplete request is received during eager accept or eager keep-alive.
|
|
186
|
+
#
|
|
187
|
+
# The persisted flag selects between persistent_data_timeout (for
|
|
188
|
+
# kept-alive connections awaiting the next request) and chunk_data_timeout
|
|
189
|
+
# (for fresh connections awaiting the rest of the first request).
|
|
159
190
|
#
|
|
160
191
|
# @param socket [TCPSocket] the client socket
|
|
161
192
|
# @param id [Integer] unique client identifier
|
|
@@ -166,10 +197,11 @@ module Raptor
|
|
|
166
197
|
# @param request_count [Integer] number of requests handled on this connection
|
|
167
198
|
# @param remote_addr [String] client IP address
|
|
168
199
|
# @param url_scheme [String] "http" or "https"
|
|
200
|
+
# @param persisted [Boolean] whether the connection has already completed at least one request
|
|
169
201
|
# @return [void]
|
|
170
202
|
#
|
|
171
|
-
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
172
|
-
def fallback_to_reactor: (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
203
|
+
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
204
|
+
def fallback_to_reactor: (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
173
205
|
|
|
174
206
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
175
207
|
#
|
|
@@ -10,16 +10,19 @@ module Raptor
|
|
|
10
10
|
#
|
|
11
11
|
# Supports TCP, Unix domain, and SSL listeners transparently. TCP_NODELAY is
|
|
12
12
|
# applied only to TCP sockets, and SSL handshakes are performed synchronously
|
|
13
|
-
# before
|
|
13
|
+
# before the connection is dispatched.
|
|
14
14
|
#
|
|
15
|
-
# For
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# For HTTP/1.1 connections the first request is parsed inline on the server
|
|
16
|
+
# thread and dispatched directly to the thread pool, falling back to the
|
|
17
|
+
# reactor only when more data is needed. For HTTP/2 connections (negotiated
|
|
18
|
+
# via ALPN) the server sends initial SETTINGS and registers the connection
|
|
19
|
+
# with the reactor for frame processing through the ractor pool.
|
|
18
20
|
#
|
|
19
21
|
# @example
|
|
20
22
|
# binder = Binder.new(["tcp://0.0.0.0:3000"])
|
|
21
23
|
# reactor = Reactor.new(thread_pool, ractor_pool, client_options: {})
|
|
22
|
-
#
|
|
24
|
+
# request = Request.new(app, 3000)
|
|
25
|
+
# server = Server.new(binder, reactor, thread_pool, request)
|
|
23
26
|
# server.run
|
|
24
27
|
# # ... later
|
|
25
28
|
# server.shutdown
|
|
@@ -36,6 +39,8 @@ module Raptor
|
|
|
36
39
|
|
|
37
40
|
@thread_pool: AtomicThreadPool
|
|
38
41
|
|
|
42
|
+
@request: Request
|
|
43
|
+
|
|
39
44
|
@running: AtomicBoolean
|
|
40
45
|
|
|
41
46
|
# Creates a new Server instance.
|
|
@@ -43,10 +48,11 @@ module Raptor
|
|
|
43
48
|
# @param binder [Binder] the binder managing listening sockets
|
|
44
49
|
# @param reactor [Reactor] the reactor for handling client connections
|
|
45
50
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
51
|
+
# @param request [Request] the HTTP/1.1 request handler
|
|
46
52
|
# @return [void]
|
|
47
53
|
#
|
|
48
|
-
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
49
|
-
def initialize: (Binder binder, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
54
|
+
# @rbs (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
|
|
55
|
+
def initialize: (Binder binder, Reactor reactor, AtomicThreadPool thread_pool, Request request) -> void
|
|
50
56
|
|
|
51
57
|
# Starts the server's main accept loop in a new thread.
|
|
52
58
|
#
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Young
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- ext/raptor_http2/extconf.rb
|
|
115
115
|
- ext/raptor_http2/huffman_table.h
|
|
116
116
|
- ext/raptor_http2/raptor_http2.c
|
|
117
|
+
- lib/rackup/handler/raptor.rb
|
|
117
118
|
- lib/raptor.rb
|
|
118
119
|
- lib/raptor/binder.rb
|
|
119
120
|
- lib/raptor/cli.rb
|
|
@@ -124,6 +125,7 @@ files:
|
|
|
124
125
|
- lib/raptor/server.rb
|
|
125
126
|
- lib/raptor/stats.rb
|
|
126
127
|
- lib/raptor/version.rb
|
|
128
|
+
- sig/generated/rackup/handler/raptor.rbs
|
|
127
129
|
- sig/generated/raptor.rbs
|
|
128
130
|
- sig/generated/raptor/binder.rbs
|
|
129
131
|
- sig/generated/raptor/cli.rbs
|