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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39d5ba6964929954d0c4b4cafa93249bd71c1750a56a07f2947fee2e493f037f
4
- data.tar.gz: b9bd3c49e4dfefc990e50d14b631b1d73d697da3d236ee5c38424f57e6c686cd
3
+ metadata.gz: d4a7acc08848f71017602d3abfb38b7d352a48d7bc7929e54f3a693dc3a0fe83
4
+ data.tar.gz: 1ee020abeb67db2a2eba6ff64f59054bf2e91447bd3996464f5a3279c3d3e748
5
5
  SHA512:
6
- metadata.gz: dac6fd0c4f5e0005a159103c9d27986cf322ffc44da4f5e4481751d3e38318c521fa094eb83134592c12132ab963336a75bf0350e82e59cefda4a9703b830314
7
- data.tar.gz: 2d24b52bc8700740d9ee479894159e8434012036c494889f012efbf1e5b90ea0e03968a09a5270330939f402148ea63fc4924c1951e7743e59cf4957bcec6f6c
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.5.1
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.5.1 vs Puma 8.0.2:
65
+ Raptor 0.6.0 vs Puma 8.0.2:
66
66
 
67
67
  | Protocol | Raptor | Puma |
68
68
  | --------------------- | ----------- | ----------- |
69
- | HTTP/1.1 | 20.1k req/s | 20k req/s |
70
- | HTTP/1.1 (keep-alive) | 61.4k req/s | 39.1k req/s |
71
- | HTTP/2 | 22.8k req/s | N/A |
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
@@ -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 = "Raptor Stats File"
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(name: "Raptor Workers", size: @thread_count)
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 = "Raptor Stats"
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 = Set.new(%w[connection transfer-encoding keep-alive upgrade proxy-connection]).freeze
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
- if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
519
- apply_flow_control_updates(flow_control, result)
520
- end
521
-
522
- writer.write_frames(socket, result[:outgoing_frames])
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
- if result[:close_connection]
525
- reactor.close_connection(result[:id])
526
- return
527
- end
530
+ writer.write_frames(socket, result[:outgoing_frames])
528
531
 
529
- reactor.update_http2_state(result)
532
+ if result[:close_connection]
533
+ reactor.close_connection(result[:id])
534
+ return
535
+ end
530
536
 
531
- result[:completed_requests]&.each do |request|
532
- stream_id = request[:stream_id]
533
- remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
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
- thread_pool << proc do
536
- dispatch_stream_request(
537
- socket, writer, flow_control, stream_id,
538
- request[:headers], request[:body],
539
- remote_addr: remote_addr
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 `main`.
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 || "main"
50
- thread = Thread.current.name || "main"
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
@@ -116,7 +116,7 @@ module Raptor
116
116
  # @rbs () -> Thread
117
117
  def run
118
118
  Thread.new do
119
- Thread.current.name = self.class.name
119
+ Thread.current.name = "Reactor"
120
120
 
121
121
  until @queue.closed? && @queue.empty?
122
122
  begin
@@ -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 = Set.new([204, 304, *100..199]).freeze
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 nil unless array.is_a?(Array)
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 = self.class.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)
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.5.1"
5
+ VERSION = "0.7.0"
6
6
  end
@@ -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 `main`.
34
+ # and thread. Unnamed ractors and threads are reported as `Main`.
35
35
  #
36
36
  # @return [String] the prefix
37
37
  #
@@ -31,6 +31,8 @@ module Raptor
31
31
 
32
32
  DEFAULT_SERVER_NAME: ::String
33
33
 
34
+ MIN_BACKPRESSURE_THRESHOLD: ::Integer
35
+
34
36
  @running: AtomicBoolean
35
37
 
36
38
  @client_options: Hash[Symbol, untyped]
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.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young