raptor 0.5.1 → 0.7.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 +8 -0
- data/README.md +5 -5
- data/lib/raptor/cluster.rb +3 -4
- data/lib/raptor/http2.rb +64 -21
- data/lib/raptor/log.rb +3 -3
- data/lib/raptor/reactor.rb +1 -1
- data/lib/raptor/request.rb +2 -2
- data/lib/raptor/server.rb +4 -2
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/http2.rbs +18 -1
- data/sig/generated/raptor/log.rbs +1 -1
- data/sig/generated/raptor/server.rbs +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4a7acc08848f71017602d3abfb38b7d352a48d7bc7929e54f3a693dc3a0fe83
|
|
4
|
+
data.tar.gz: 1ee020abeb67db2a2eba6ff64f59054bf2e91447bd3996464f5a3279c3d3e748
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5b25843f29afe7e47a90c85f48ba5f9d06e7f5dc819d24595821940f8207fa46054208f38601a455271d026246bfd32144329782f1a842f44cd500bf3d10bbf
|
|
7
|
+
data.tar.gz: 5c1f9f18fbf946a8e4a648bed675f83ba7d71bdc9a85354b5df1ccc6ba39bd65b5740c31e1ffd11f4f88960b52aaee0ce2b46747fac52907bae6b6598f4e390e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-06-12
|
|
4
|
+
|
|
5
|
+
- Eagerly consume back-to-back HTTP/2 frame batches in the pipeline collector
|
|
6
|
+
|
|
7
|
+
## [0.6.0] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
- Raise the backpressure threshold floor so low thread counts don't throttle prematurely
|
|
10
|
+
|
|
3
11
|
## [0.5.1] - 2026-05-31
|
|
4
12
|
|
|
5
13
|
- Fix `LoadError` when requiring the native extensions from an installed gem
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
31
31
|
```
|
|
32
32
|
> bundle exec raptor -w 4 -t 3 hello_world.ru
|
|
33
33
|
[Raptor 91348|main|main] Cluster initializing:
|
|
34
|
-
[Raptor 91348|main|main] ├─ Version: 0.
|
|
34
|
+
[Raptor 91348|main|main] ├─ Version: 0.6.0
|
|
35
35
|
[Raptor 91348|main|main] ├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
36
36
|
[Raptor 91348|main|main] ├─ Master PID: 91348
|
|
37
37
|
[Raptor 91348|main|main] │ └─ 4 worker processes
|
|
@@ -62,13 +62,13 @@ Also works with `rackup` and `rails server`:
|
|
|
62
62
|
|
|
63
63
|
## (Micro) Benchmarks
|
|
64
64
|
|
|
65
|
-
Raptor 0.
|
|
65
|
+
Raptor 0.6.0 vs Puma 8.0.2:
|
|
66
66
|
|
|
67
67
|
| Protocol | Raptor | Puma |
|
|
68
68
|
| --------------------- | ----------- | ----------- |
|
|
69
|
-
| HTTP/1.1 |
|
|
70
|
-
| HTTP/1.1 (keep-alive) |
|
|
71
|
-
| HTTP/2 |
|
|
69
|
+
| HTTP/1.1 | 17.9k req/s | 16.8k req/s |
|
|
70
|
+
| HTTP/1.1 (keep-alive) | 60k req/s | 29.6k req/s |
|
|
71
|
+
| HTTP/2 | 57.2k req/s | N/A |
|
|
72
72
|
|
|
73
73
|
> ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
74
74
|
> 4 workers, 3 threads, 12 concurrent connections
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -156,7 +156,7 @@ module Raptor
|
|
|
156
156
|
|
|
157
157
|
stats_file_thread = if @stats_file
|
|
158
158
|
Thread.new do
|
|
159
|
-
Thread.current.name = "
|
|
159
|
+
Thread.current.name = "Stats File Writer"
|
|
160
160
|
|
|
161
161
|
write_stats_file_loop
|
|
162
162
|
end
|
|
@@ -353,11 +353,10 @@ module Raptor
|
|
|
353
353
|
request_count += 1
|
|
354
354
|
@app.call(env)
|
|
355
355
|
}
|
|
356
|
-
thread_pool = AtomicThreadPool.new(
|
|
356
|
+
thread_pool = AtomicThreadPool.new(size: @thread_count)
|
|
357
357
|
request = Request.new(counting_app, @server_port, client_options: @client_options, on_error: @on_error)
|
|
358
358
|
http2 = Http2.new(counting_app, @server_port, on_error: @on_error)
|
|
359
359
|
ractor_pool = RactorPool.new(
|
|
360
|
-
name: "Raptor Pipeline Workers",
|
|
361
360
|
size: @ractor_count,
|
|
362
361
|
worker: request.http_parser_worker
|
|
363
362
|
) do |parsed_result|
|
|
@@ -381,7 +380,7 @@ module Raptor
|
|
|
381
380
|
Log.info "Worker #{index} booted"
|
|
382
381
|
|
|
383
382
|
stats_thread = Thread.new do
|
|
384
|
-
Thread.current.name = "
|
|
383
|
+
Thread.current.name = "Stats Writer"
|
|
385
384
|
|
|
386
385
|
loop do
|
|
387
386
|
@stats.write(
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -223,6 +223,10 @@ module Raptor
|
|
|
223
223
|
end
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
EAGER_READ_TIMEOUT = 0.001
|
|
227
|
+
EAGER_READ_BUFFER_SIZE = 64 * 1024
|
|
228
|
+
EAGER_MAX_ROUNDS = 4
|
|
229
|
+
|
|
226
230
|
FLAG_END_STREAM = 0x1
|
|
227
231
|
FLAG_END_HEADERS = 0x4
|
|
228
232
|
FLAG_ACK = 0x1
|
|
@@ -236,7 +240,7 @@ module Raptor
|
|
|
236
240
|
|
|
237
241
|
SERVER_PROTOCOL = "HTTP/2"
|
|
238
242
|
RACK_HEADER_PREFIX = "rack."
|
|
239
|
-
HOP_BY_HOP_HEADERS =
|
|
243
|
+
HOP_BY_HOP_HEADERS = ["connection", "transfer-encoding", "keep-alive", "upgrade", "proxy-connection"].freeze
|
|
240
244
|
|
|
241
245
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
242
246
|
# @rbs @server_port: Integer
|
|
@@ -500,7 +504,9 @@ module Raptor
|
|
|
500
504
|
# Handles a parsed HTTP/2 request from the ractor pool.
|
|
501
505
|
#
|
|
502
506
|
# Writes outgoing protocol frames to the socket, updates reactor state,
|
|
503
|
-
# and dispatches completed stream requests to the thread pool.
|
|
507
|
+
# and dispatches completed stream requests to the thread pool. Eagerly
|
|
508
|
+
# consumes subsequent frame batches that are already buffered, skipping
|
|
509
|
+
# the reactor and ractor pool hops while the connection is hot.
|
|
504
510
|
#
|
|
505
511
|
# @param result [Hash] the parsed result from the ractor pool
|
|
506
512
|
# @param reactor [Reactor] the reactor managing the connection
|
|
@@ -515,31 +521,42 @@ module Raptor
|
|
|
515
521
|
writer = reactor.writer_for(result[:id])
|
|
516
522
|
flow_control = reactor.flow_control_for(result[:id])
|
|
517
523
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
rounds = 0
|
|
525
|
+
loop do
|
|
526
|
+
if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
|
|
527
|
+
apply_flow_control_updates(flow_control, result)
|
|
528
|
+
end
|
|
523
529
|
|
|
524
|
-
|
|
525
|
-
reactor.close_connection(result[:id])
|
|
526
|
-
return
|
|
527
|
-
end
|
|
530
|
+
writer.write_frames(socket, result[:outgoing_frames])
|
|
528
531
|
|
|
529
|
-
|
|
532
|
+
if result[:close_connection]
|
|
533
|
+
reactor.close_connection(result[:id])
|
|
534
|
+
return
|
|
535
|
+
end
|
|
530
536
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
537
|
+
result[:completed_requests]&.each do |request|
|
|
538
|
+
stream_id = request[:stream_id]
|
|
539
|
+
remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
534
540
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
+
thread_pool << proc do
|
|
542
|
+
dispatch_stream_request(
|
|
543
|
+
socket, writer, flow_control, stream_id,
|
|
544
|
+
request[:headers], request[:body],
|
|
545
|
+
remote_addr: remote_addr
|
|
546
|
+
)
|
|
547
|
+
end
|
|
541
548
|
end
|
|
549
|
+
|
|
550
|
+
rounds += 1
|
|
551
|
+
break if rounds >= EAGER_MAX_ROUNDS
|
|
552
|
+
|
|
553
|
+
next_batch = eager_read_next_batch(socket)
|
|
554
|
+
break unless next_batch
|
|
555
|
+
|
|
556
|
+
result = Raptor::Http2.process_frames(result.merge(buffer: result[:buffer] + next_batch))
|
|
542
557
|
end
|
|
558
|
+
|
|
559
|
+
reactor.update_http2_state(result)
|
|
543
560
|
end
|
|
544
561
|
|
|
545
562
|
private
|
|
@@ -566,6 +583,32 @@ module Raptor
|
|
|
566
583
|
end
|
|
567
584
|
end
|
|
568
585
|
|
|
586
|
+
# Reads the next frame batch from `socket` within a short window, or
|
|
587
|
+
# returns nil if nothing arrives in time.
|
|
588
|
+
#
|
|
589
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
590
|
+
# @return [String, nil] the bytes read, or nil if nothing was available
|
|
591
|
+
#
|
|
592
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
593
|
+
def eager_read_next_batch(socket)
|
|
594
|
+
return unless socket.wait_readable(EAGER_READ_TIMEOUT)
|
|
595
|
+
|
|
596
|
+
data = begin
|
|
597
|
+
socket.read_nonblock(EAGER_READ_BUFFER_SIZE)
|
|
598
|
+
rescue IO::WaitReadable, EOFError, IOError
|
|
599
|
+
return
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
buffer = String.new
|
|
603
|
+
buffer << data
|
|
604
|
+
|
|
605
|
+
while socket.pending > 0
|
|
606
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
buffer
|
|
610
|
+
end
|
|
611
|
+
|
|
569
612
|
# Dispatches a completed stream request to the Rack app and writes
|
|
570
613
|
# the response back as HTTP/2 frames.
|
|
571
614
|
#
|
data/lib/raptor/log.rb
CHANGED
|
@@ -40,14 +40,14 @@ module Raptor
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Builds the log line prefix from the current process, ractor,
|
|
43
|
-
# and thread. Unnamed ractors and threads are reported as `
|
|
43
|
+
# and thread. Unnamed ractors and threads are reported as `Main`.
|
|
44
44
|
#
|
|
45
45
|
# @return [String] the prefix
|
|
46
46
|
#
|
|
47
47
|
# @rbs () -> String
|
|
48
48
|
def self.prefix
|
|
49
|
-
ractor = Ractor.current.name || "
|
|
50
|
-
thread = Thread.current.name || "
|
|
49
|
+
ractor = Ractor.current.name || "Main"
|
|
50
|
+
thread = Thread.current.name || "Main"
|
|
51
51
|
"[Raptor #{Process.pid}|#{ractor}|#{thread}]"
|
|
52
52
|
end
|
|
53
53
|
private_class_method :prefix
|
data/lib/raptor/reactor.rb
CHANGED
data/lib/raptor/request.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Raptor
|
|
|
35
35
|
h[status] = "HTTP/1.1 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
STATUS_WITH_NO_ENTITY_BODY =
|
|
38
|
+
STATUS_WITH_NO_ENTITY_BODY = [204, 304, *100..199].freeze
|
|
39
39
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
40
40
|
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
41
41
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
@@ -947,7 +947,7 @@ module Raptor
|
|
|
947
947
|
def calculate_content_length(body)
|
|
948
948
|
if body.respond_to?(:to_ary)
|
|
949
949
|
array = body.to_ary
|
|
950
|
-
return
|
|
950
|
+
return unless array.is_a?(Array)
|
|
951
951
|
|
|
952
952
|
array.sum { |chunk| chunk.is_a?(String) ? chunk.bytesize : 0 }
|
|
953
953
|
elsif body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
data/lib/raptor/server.rb
CHANGED
|
@@ -35,6 +35,8 @@ module Raptor
|
|
|
35
35
|
DEFAULT_REMOTE_ADDR = "127.0.0.1"
|
|
36
36
|
DEFAULT_SERVER_NAME = "localhost"
|
|
37
37
|
|
|
38
|
+
MIN_BACKPRESSURE_THRESHOLD = 64
|
|
39
|
+
|
|
38
40
|
# @rbs @binder: Binder
|
|
39
41
|
# @rbs @reactor: Reactor
|
|
40
42
|
# @rbs @thread_pool: AtomicThreadPool
|
|
@@ -73,7 +75,7 @@ module Raptor
|
|
|
73
75
|
# @rbs () -> Thread
|
|
74
76
|
def run
|
|
75
77
|
Thread.new(@binder.listeners, @reactor, @running) do |server_sockets, reactor, running|
|
|
76
|
-
Thread.current.name =
|
|
78
|
+
Thread.current.name = "Server"
|
|
77
79
|
|
|
78
80
|
while running.true?
|
|
79
81
|
begin
|
|
@@ -83,7 +85,7 @@ module Raptor
|
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
next unless ready_servers
|
|
86
|
-
next if @reactor.backlog >= (@thread_pool.size * 1.2).ceil
|
|
88
|
+
next if @reactor.backlog >= [(@thread_pool.size * 1.2).ceil, MIN_BACKPRESSURE_THRESHOLD].max
|
|
87
89
|
|
|
88
90
|
ready_servers.each do |listener|
|
|
89
91
|
accept_connection(listener, reactor)
|
data/lib/raptor/version.rb
CHANGED
|
@@ -112,6 +112,12 @@ module Raptor
|
|
|
112
112
|
def discard_stream: (Integer stream_id) -> void
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
EAGER_READ_TIMEOUT: ::Float
|
|
116
|
+
|
|
117
|
+
EAGER_READ_BUFFER_SIZE: untyped
|
|
118
|
+
|
|
119
|
+
EAGER_MAX_ROUNDS: ::Integer
|
|
120
|
+
|
|
115
121
|
FLAG_END_STREAM: ::Integer
|
|
116
122
|
|
|
117
123
|
FLAG_END_HEADERS: ::Integer
|
|
@@ -205,7 +211,9 @@ module Raptor
|
|
|
205
211
|
# Handles a parsed HTTP/2 request from the ractor pool.
|
|
206
212
|
#
|
|
207
213
|
# Writes outgoing protocol frames to the socket, updates reactor state,
|
|
208
|
-
# and dispatches completed stream requests to the thread pool.
|
|
214
|
+
# and dispatches completed stream requests to the thread pool. Eagerly
|
|
215
|
+
# consumes subsequent frame batches that are already buffered, skipping
|
|
216
|
+
# the reactor and ractor pool hops while the connection is hot.
|
|
209
217
|
#
|
|
210
218
|
# @param result [Hash] the parsed result from the ractor pool
|
|
211
219
|
# @param reactor [Reactor] the reactor managing the connection
|
|
@@ -227,6 +235,15 @@ module Raptor
|
|
|
227
235
|
# @rbs (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
|
|
228
236
|
def apply_flow_control_updates: (FlowControl flow_control, Hash[Symbol, untyped] result) -> void
|
|
229
237
|
|
|
238
|
+
# Reads the next frame batch from `socket` within a short window, or
|
|
239
|
+
# returns nil if nothing arrives in time.
|
|
240
|
+
#
|
|
241
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
242
|
+
# @return [String, nil] the bytes read, or nil if nothing was available
|
|
243
|
+
#
|
|
244
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
245
|
+
def eager_read_next_batch: (OpenSSL::SSL::SSLSocket socket) -> String?
|
|
246
|
+
|
|
230
247
|
# Dispatches a completed stream request to the Rack app and writes
|
|
231
248
|
# the response back as HTTP/2 frames.
|
|
232
249
|
#
|
|
@@ -31,7 +31,7 @@ module Raptor
|
|
|
31
31
|
def self.rescued_error: (Exception error) -> void
|
|
32
32
|
|
|
33
33
|
# Builds the log line prefix from the current process, ractor,
|
|
34
|
-
# and thread. Unnamed ractors and threads are reported as `
|
|
34
|
+
# and thread. Unnamed ractors and threads are reported as `Main`.
|
|
35
35
|
#
|
|
36
36
|
# @return [String] the prefix
|
|
37
37
|
#
|