raptor 0.6.0 → 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: 7a43fe40aecdcdd5ff72bf3ecfde433611877d563e483163b868efcf0870ae39
4
- data.tar.gz: 9e7e6761bb6cee495f6081715461cc10b4c8ebf1e6480e7f5b1c80715d28337b
3
+ metadata.gz: d4a7acc08848f71017602d3abfb38b7d352a48d7bc7929e54f3a693dc3a0fe83
4
+ data.tar.gz: 1ee020abeb67db2a2eba6ff64f59054bf2e91447bd3996464f5a3279c3d3e748
5
5
  SHA512:
6
- metadata.gz: 5f792cac133140519597d727972b649781288fc2ccc4d60243bf53502a33b8a764494e2b148f1f992a3af42002a8b2b52d535dad8ee988bd7725f100541ed6ff
7
- data.tar.gz: 1a0715da97c2c325ba70e5daa77c89d6169991034bd83f78ba1f86956e996e0a153b468e296288ba4d2a991499319628231d81b3695db2a57b8e8138ee4ac094
6
+ metadata.gz: b5b25843f29afe7e47a90c85f48ba5f9d06e7f5dc819d24595821940f8207fa46054208f38601a455271d026246bfd32144329782f1a842f44cd500bf3d10bbf
7
+ data.tar.gz: 5c1f9f18fbf946a8e4a648bed675f83ba7d71bdc9a85354b5df1ccc6ba39bd65b5740c31e1ffd11f4f88960b52aaee0ce2b46747fac52907bae6b6598f4e390e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## [0.6.0] - 2026-06-02
4
8
 
5
9
  - Raise the backpressure threshold floor so low thread counts don't throttle prematurely
data/README.md CHANGED
@@ -66,9 +66,9 @@ Raptor 0.6.0 vs Puma 8.0.2:
66
66
 
67
67
  | Protocol | Raptor | Puma |
68
68
  | --------------------- | ----------- | ----------- |
69
- | HTTP/1.1 | 20.2k req/s | 20k req/s |
70
- | HTTP/1.1 (keep-alive) | 61.4k req/s | 39.2k 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
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
  #
@@ -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)
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.6.0"
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
  #
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young