raptor 0.3.0 → 0.5.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.
@@ -9,12 +9,12 @@ module Raptor
9
9
  #
10
10
  # Reactor uses NIO selectors for efficient I/O multiplexing and implements
11
11
  # client timeouts using a red-black tree for O(log n) timeout management.
12
- # It coordinates between thread pools for blocking operations and ractor
13
- # pools for CPU-intensive HTTP parsing, and provides backlog metrics
14
- # that the server uses for backpressure control to prevent overload.
12
+ # It coordinates between ractor pools for CPU-intensive HTTP parsing and
13
+ # thread pools for blocking operations, and provides backlog metrics that
14
+ # the server uses for backpressure control to prevent overload.
15
15
  #
16
16
  # @example
17
- # reactor = Reactor.new(thread_pool, ractor_pool, client_options: {
17
+ # reactor = Reactor.new(ractor_pool, thread_pool, client_options: {
18
18
  # first_data_timeout: 30,
19
19
  # chunk_data_timeout: 10
20
20
  # })
@@ -24,38 +24,38 @@ module Raptor
24
24
  # reactor.shutdown
25
25
  #
26
26
  class Reactor
27
- # Red-black tree node representing a client connection with timeout tracking.
28
- #
29
- # TimeoutClient extends RedBlackTree::Node to enable efficient timeout
30
- # management using the tree's ordering properties.
27
+ # A client connection node ordered by absolute expiry time so the
28
+ # soonest-to-expire is always at the tree's minimum.
31
29
  #
32
30
  class TimeoutClient < RedBlackTree::Node
33
31
  # @rbs attr_accessor timeout_at: Float
34
32
  attr_accessor :timeout_at
35
33
 
36
- # Returns the client data stored in this timeout node.
34
+ # Semantic alias for the inherited `data` slot.
37
35
  #
38
- # @return [Hash] the client connection state data
36
+ # @return [Hash] the client connection state
39
37
  #
40
38
  # @rbs () -> Hash[Symbol, untyped]
41
39
  def client_data
42
40
  data
43
41
  end
44
42
 
45
- # Calculates remaining timeout duration from the current time.
43
+ # Returns seconds until expiry, clamped to 0 so an already-expired
44
+ # client doesn't push the next selector wait into the future.
46
45
  #
47
46
  # @param now [Float] current monotonic timestamp
48
- # @return [Float] remaining timeout in seconds, minimum 0
47
+ # @return [Float] seconds until expiry, never negative
49
48
  #
50
49
  # @rbs (Float now) -> Float
51
50
  def timeout(now)
52
51
  [timeout_at - now, 0].max
53
52
  end
54
53
 
55
- # Compares timeout nodes by their timeout_at values for tree ordering.
54
+ # Orders nodes by `timeout_at` so the tree minimum is the next
55
+ # client to expire.
56
56
  #
57
57
  # @param other [TimeoutClient] another timeout client to compare
58
- # @return [Integer] -1, 0, or 1 for ordering
58
+ # @return [Integer] -1, 0, or 1
59
59
  #
60
60
  # @rbs (TimeoutClient other) -> Integer
61
61
  def <=>(other)
@@ -76,21 +76,22 @@ module Raptor
76
76
  # @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
77
77
  # @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
78
78
  # @rbs @id_to_writer: Hash[Integer, untyped]
79
+ # @rbs @id_to_flow_control: Hash[Integer, untyped]
79
80
 
80
81
  # Creates a new Reactor instance.
81
82
  #
82
- # @param thread_pool [AtomicThreadPool] thread pool for application processing
83
83
  # @param ractor_pool [RactorPool] ractor pool for HTTP parsing
84
+ # @param thread_pool [AtomicThreadPool] thread pool for application processing
84
85
  # @param client_options [Hash] timeout configuration options
85
86
  # @option client_options [Integer] :first_data_timeout timeout for initial data
86
87
  # @option client_options [Integer] :chunk_data_timeout timeout for subsequent chunks
87
88
  # @option client_options [Integer] :persistent_data_timeout timeout for keep-alive connections
88
89
  # @return [void]
89
90
  #
90
- # @rbs (untyped thread_pool, untyped ractor_pool, client_options: Hash[Symbol, Integer]) -> void
91
- def initialize(thread_pool, ractor_pool, client_options:)
92
- @thread_pool = thread_pool
91
+ # @rbs (untyped ractor_pool, untyped thread_pool, client_options: Hash[Symbol, Integer]) -> void
92
+ def initialize(ractor_pool, thread_pool, client_options:)
93
93
  @ractor_pool = ractor_pool
94
+ @thread_pool = thread_pool
94
95
  @client_options = client_options
95
96
 
96
97
  @selector = NIO::Selector.new
@@ -101,6 +102,7 @@ module Raptor
101
102
  @socket_to_state = {}
102
103
  @id_to_timeout = {}
103
104
  @id_to_writer = {}
105
+ @id_to_flow_control = {}
104
106
  end
105
107
 
106
108
  # Starts the reactor's main event loop in a new thread.
@@ -117,33 +119,37 @@ module Raptor
117
119
  Thread.current.name = self.class.name
118
120
 
119
121
  until @queue.closed? && @queue.empty?
120
- timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
121
- @selector.select(timeout) do |monitor|
122
- wakeup!(monitor.value)
123
- end
124
-
125
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
- expired = []
127
- @timeouts.traverse do |to_client|
128
- break unless to_client.timeout(now) == 0
129
-
130
- expired << to_client
131
- end
132
-
133
- expired.each do |to_client|
134
- @timeouts.delete!(to_client)
135
- id = to_client.client_data[:id]
136
- @id_to_timeout.delete(id)
137
- socket = @id_to_socket[id]
138
- next unless socket
139
-
140
- @selector.deregister(socket)
141
- socket.write(TIMEOUT_RESPONSE) rescue nil
142
- cleanup(socket)
143
- end
144
-
145
- until @queue.empty?
146
- register(@queue.pop)
122
+ begin
123
+ timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
124
+ @selector.select(timeout) do |monitor|
125
+ wakeup!(monitor.value)
126
+ end
127
+
128
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
+ expired = []
130
+ @timeouts.traverse do |to_client|
131
+ break unless to_client.timeout(now) == 0
132
+
133
+ expired << to_client
134
+ end
135
+
136
+ expired.each do |to_client|
137
+ @timeouts.delete!(to_client)
138
+ id = to_client.client_data[:id]
139
+ @id_to_timeout.delete(id)
140
+ socket = @id_to_socket[id]
141
+ next unless socket
142
+
143
+ @selector.deregister(socket)
144
+ socket.write(TIMEOUT_RESPONSE) rescue nil
145
+ cleanup(socket)
146
+ end
147
+
148
+ until @queue.empty?
149
+ register(@queue.pop)
150
+ end
151
+ rescue => error
152
+ Log.rescued_error(error)
147
153
  end
148
154
  end
149
155
 
@@ -163,9 +169,11 @@ module Raptor
163
169
  socket = state[:socket]
164
170
  state.delete(:socket)
165
171
  writer = state.delete(:writer)
172
+ flow_control = state.delete(:flow_control)
166
173
  @id_to_socket[state[:id]] = socket
167
174
  @socket_to_state[socket] = state
168
175
  @id_to_writer[state[:id]] = writer if writer
176
+ @id_to_flow_control[state[:id]] = flow_control if flow_control
169
177
 
170
178
  read_and_queue_for_parse(socket, state)
171
179
  end
@@ -191,13 +199,12 @@ module Raptor
191
199
  socket.close
192
200
  end
193
201
 
194
- # Removes a client connection from the reactor.
195
- #
196
- # Called when an HTTP request is complete and ready for application
197
- # processing. Triggers server accept re-enabling if system capacity allows.
202
+ # Drops the reactor's references to a client whose parsed request
203
+ # has been handed off to the thread pool. The socket itself is kept
204
+ # open so the worker can write the response.
198
205
  #
199
206
  # @param id [Integer] unique client identifier
200
- # @return [TCPSocket, nil] the removed socket, if found
207
+ # @return [TCPSocket, nil] the socket associated with `id`, if any
201
208
  #
202
209
  # @rbs (Integer id) -> TCPSocket?
203
210
  def remove(id)
@@ -262,6 +269,18 @@ module Raptor
262
269
  @id_to_writer[id]
263
270
  end
264
271
 
272
+ # Returns the flow controller associated with a given connection, if one
273
+ # was supplied when the connection was added. Used by HTTP/2 stream
274
+ # dispatchers to honour the peer's flow-control windows.
275
+ #
276
+ # @param id [Integer] unique client identifier
277
+ # @return [Object, nil] the flow controller, if found
278
+ #
279
+ # @rbs (Integer id) -> untyped?
280
+ def flow_control_for(id)
281
+ @id_to_flow_control[id]
282
+ end
283
+
265
284
  # Updates connection state for an HTTP/2 connection after frame processing.
266
285
  #
267
286
  # Re-registers the socket with the selector for further reads and stores
@@ -282,10 +301,26 @@ module Raptor
282
301
  socket.close
283
302
  end
284
303
 
285
- # Initiates reactor shutdown.
304
+ # Closes the socket for the given connection and drops all reactor state
305
+ # associated with it. Used to terminate HTTP/2 connections after sending
306
+ # a GOAWAY frame.
286
307
  #
287
- # Closes the registration queue and wakes up the selector to begin
288
- # graceful shutdown process.
308
+ # @param id [Integer] unique client identifier
309
+ # @return [void]
310
+ #
311
+ # @rbs (Integer id) -> void
312
+ def close_connection(id)
313
+ socket = @id_to_socket.delete(id)
314
+ return unless socket
315
+
316
+ @socket_to_state.delete(socket)
317
+ @id_to_writer.delete(id)
318
+ @id_to_flow_control.delete(id)
319
+ socket.close rescue nil
320
+ end
321
+
322
+ # Closes the registration queue and wakes the selector so the
323
+ # event loop drains pending work and exits.
289
324
  #
290
325
  # @return [void]
291
326
  #
@@ -385,6 +420,7 @@ module Raptor
385
420
  state = @socket_to_state.delete(socket)
386
421
  @id_to_socket.delete(state[:id])
387
422
  @id_to_writer.delete(state[:id])
423
+ @id_to_flow_control.delete(state[:id])
388
424
  socket.close
389
425
  end
390
426
 
@@ -5,18 +5,16 @@ require "socket"
5
5
  require "stringio"
6
6
  require "tempfile"
7
7
 
8
+ require "atomic-ruby/atomic_boolean"
8
9
  require "rack"
9
10
 
10
11
  require_relative "raptor_http"
11
12
 
12
13
  module Raptor
13
- # Handles HTTP request processing and Rack application integration.
14
- #
15
- # Request manages the HTTP parsing pipeline using Ractors and coordinates
16
- # with the reactor for connection state management. It bridges between the
17
- # low-level HTTP parsing and high-level Rack application interface, handling
18
- # both incomplete requests (that need more data) and complete requests
19
- # (ready for application processing).
14
+ # Parses HTTP/1.x requests and dispatches them to the Rack
15
+ # application. Coordinates with the Ractor pool for parsing and
16
+ # with the reactor for requests that need more data before they
17
+ # can be handled.
20
18
  #
21
19
  class Request
22
20
  BODY_BUFFER_THRESHOLD = 256 * 1024
@@ -26,7 +24,6 @@ module Raptor
26
24
  KEEPALIVE_READ_TIMEOUT = 0.001
27
25
  MAX_KEEPALIVE_REQUESTS = 100
28
26
 
29
- HTTP_SCHEME = "http"
30
27
  HTTP_10 = "HTTP/1.0"
31
28
  HTTP_11 = "HTTP/1.1"
32
29
  STATUS_LINE_CACHE_10 = Hash.new do |h, status|
@@ -39,6 +36,7 @@ module Raptor
39
36
  end
40
37
 
41
38
  STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
39
+ BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
42
40
  INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
43
41
  CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
44
42
 
@@ -92,11 +90,37 @@ module Raptor
92
90
  [decoded, :incomplete]
93
91
  end
94
92
 
93
+ # Writes `string` in full, retrying on partial writes. Bounded by
94
+ # `WRITE_TIMEOUT` so a slow client can't pin the writing thread.
95
+ #
96
+ # @param socket [TCPSocket] the socket to write to
97
+ # @param string [String] the data to write
98
+ # @return [void]
99
+ # @raise [WriteError] if the socket is not writable within the timeout or raises IOError
100
+ #
101
+ # @rbs (TCPSocket socket, String string) -> void
102
+ def self.socket_write(socket, string)
103
+ bytes = 0
104
+ byte_size = string.bytesize
105
+
106
+ while bytes < byte_size
107
+ begin
108
+ bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
109
+ rescue IO::WaitWritable
110
+ raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
111
+ retry
112
+ rescue IOError
113
+ raise WriteError
114
+ end
115
+ end
116
+ end
117
+
95
118
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
96
119
  # @rbs @server_port: Integer
97
120
  # @rbs @max_body_size: Integer?
98
121
  # @rbs @body_spool_threshold: Integer?
99
122
  # @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
123
+ # @rbs @running: AtomicBoolean
100
124
 
101
125
  # Creates a new Request handler.
102
126
  #
@@ -115,6 +139,17 @@ module Raptor
115
139
  @max_body_size = client_options[:max_body_size]
116
140
  @body_spool_threshold = client_options[:body_spool_threshold]
117
141
  @on_error = on_error
142
+ @running = AtomicBoolean.new(true)
143
+ end
144
+
145
+ # Signals eager keep-alive loops to stop processing further requests on
146
+ # their connections. In-flight requests complete normally.
147
+ #
148
+ # @return [void]
149
+ #
150
+ # @rbs () -> void
151
+ def shutdown
152
+ @running.make_false
118
153
  end
119
154
 
120
155
  # Eagerly reads and parses the first request on a freshly accepted
@@ -155,7 +190,12 @@ module Raptor
155
190
 
156
191
  parser = HttpParser.new
157
192
  env = {}
158
- nread = parser.execute(env, buffer, 0)
193
+ nread = begin
194
+ parser.execute(env, buffer, 0)
195
+ rescue HttpParserError
196
+ reject_malformed(socket)
197
+ return
198
+ end
159
199
  parse_data = { parse_count: 1, content_length: parser.content_length }
160
200
 
161
201
  body = nil
@@ -210,7 +250,11 @@ module Raptor
210
250
 
211
251
  parser = Raptor::HttpParser.new
212
252
  env = {}
213
- nread = parser.execute(env, data[:buffer], 0)
253
+ nread = begin
254
+ parser.execute(env, data[:buffer], 0)
255
+ rescue Raptor::HttpParserError
256
+ next Ractor.make_shareable(data.merge(complete: true, malformed: true))
257
+ end
214
258
  parse_data = if data[:parse_data]
215
259
  data[:parse_data].dup
216
260
  else
@@ -269,13 +313,19 @@ module Raptor
269
313
  return
270
314
  end
271
315
 
316
+ if parsed_request[:malformed]
317
+ socket = reactor.remove(parsed_request[:id])
318
+ reject_malformed(socket) if socket
319
+ return
320
+ end
321
+
272
322
  unless parsed_request[:complete]
273
323
  reactor.update_state(parsed_request)
274
324
  else
275
325
  socket = reactor.remove(parsed_request[:id])
276
326
  request_count = (parsed_request[:request_count] || 0) + 1
277
- remote_addr = parsed_request[:remote_addr] || "127.0.0.1"
278
- url_scheme = parsed_request[:url_scheme] || HTTP_SCHEME
327
+ remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
328
+ url_scheme = parsed_request[:url_scheme] || Server::HTTP_SCHEME
279
329
 
280
330
  thread_pool << proc do
281
331
  process_client(
@@ -394,6 +444,11 @@ module Raptor
394
444
  # @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
395
445
  def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
396
446
  loop do
447
+ unless @running.true?
448
+ socket.close rescue nil
449
+ return
450
+ end
451
+
397
452
  unless socket.wait_readable(KEEPALIVE_READ_TIMEOUT)
398
453
  reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
399
454
  return
@@ -418,7 +473,12 @@ module Raptor
418
473
 
419
474
  parser = HttpParser.new
420
475
  env = {}
421
- nread = parser.execute(env, buffer, 0)
476
+ nread = begin
477
+ parser.execute(env, buffer, 0)
478
+ rescue HttpParserError
479
+ reject_malformed(socket)
480
+ return
481
+ end
422
482
  parse_data = { parse_count: 1, content_length: parser.content_length }
423
483
 
424
484
  body = nil
@@ -515,6 +575,18 @@ module Raptor
515
575
  socket.close rescue nil
516
576
  end
517
577
 
578
+ # Writes a 400 response and closes the socket. Used when the HTTP parser
579
+ # rejects the request line or headers.
580
+ #
581
+ # @param socket [TCPSocket] the client socket
582
+ # @return [void]
583
+ #
584
+ # @rbs (TCPSocket socket) -> void
585
+ def reject_malformed(socket)
586
+ socket.write(BAD_REQUEST_RESPONSE) rescue nil
587
+ socket.close rescue nil
588
+ end
589
+
518
590
  # Builds a Rack environment hash from parsed HTTP request data.
519
591
  #
520
592
  # Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
@@ -529,7 +601,7 @@ module Raptor
529
601
  # @return [Hash] fully populated Rack environment hash
530
602
  #
531
603
  # @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
532
- def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
604
+ def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
533
605
  env[Rack::RACK_VERSION] = Rack::VERSION
534
606
  env[Rack::RACK_URL_SCHEME] = url_scheme
535
607
  env[Rack::RACK_INPUT] = build_rack_input(body)
@@ -569,7 +641,7 @@ module Raptor
569
641
  env[Rack::SERVER_NAME] ||= host
570
642
  env[Rack::SERVER_PORT] ||= port || @server_port.to_s
571
643
  else
572
- env[Rack::SERVER_NAME] ||= "localhost"
644
+ env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
573
645
  env[Rack::SERVER_PORT] ||= @server_port.to_s
574
646
  end
575
647
 
@@ -644,7 +716,7 @@ module Raptor
644
716
  end
645
717
  response << "\r\n"
646
718
 
647
- socket_write(socket, response)
719
+ Request.socket_write(socket, response)
648
720
  end
649
721
 
650
722
  # Writes a complete HTTP response to the socket.
@@ -777,7 +849,7 @@ module Raptor
777
849
  def write_hijacked_response(socket, response, headers, response_hijack)
778
850
  response << format_headers(headers)
779
851
  response << "\r\n"
780
- socket_write(socket, response)
852
+ Request.socket_write(socket, response)
781
853
  uncork_socket(socket)
782
854
  response_hijack.call(socket)
783
855
  end
@@ -802,7 +874,7 @@ module Raptor
802
874
 
803
875
  response << format_headers(headers)
804
876
  response << "\r\n"
805
- socket_write(socket, response)
877
+ Request.socket_write(socket, response)
806
878
  end
807
879
 
808
880
  # Writes a complete response with a body.
@@ -824,7 +896,7 @@ module Raptor
824
896
  if body.respond_to?(:call)
825
897
  response << format_headers(headers)
826
898
  response << "\r\n"
827
- socket_write(socket, response)
899
+ Request.socket_write(socket, response)
828
900
  uncork_socket(socket)
829
901
  body.call(socket)
830
902
  return
@@ -861,7 +933,7 @@ module Raptor
861
933
  raise TypeError, "body must respond to each, to_ary, or to_path"
862
934
  end
863
935
 
864
- socket_write(socket, "0\r\n\r\n") if use_chunked
936
+ Request.socket_write(socket, "0\r\n\r\n") if use_chunked
865
937
  end
866
938
 
867
939
  # Calculates content length from an array or file body without consuming it.
@@ -901,15 +973,15 @@ module Raptor
901
973
  def write_file_body(socket, response, path, content_length, use_chunked)
902
974
  File.open(path, "rb") do |file|
903
975
  if use_chunked
904
- socket_write(socket, response)
976
+ Request.socket_write(socket, response)
905
977
  while (chunk = file.read(FILE_CHUNK_SIZE))
906
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
978
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
907
979
  end
908
980
  elsif content_length && content_length < BODY_BUFFER_THRESHOLD
909
981
  response << file.read(content_length)
910
- socket_write(socket, response)
982
+ Request.socket_write(socket, response)
911
983
  else
912
- socket_write(socket, response)
984
+ Request.socket_write(socket, response)
913
985
  IO.copy_stream(file, socket)
914
986
  end
915
987
  end
@@ -952,12 +1024,12 @@ module Raptor
952
1024
 
953
1025
  if use_chunked
954
1026
  response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
955
- socket_write(socket, response)
1027
+ Request.socket_write(socket, response)
956
1028
  elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
957
- socket_write(socket, response << chunk)
1029
+ Request.socket_write(socket, response << chunk)
958
1030
  else
959
- socket_write(socket, response)
960
- socket_write(socket, chunk)
1031
+ Request.socket_write(socket, response)
1032
+ Request.socket_write(socket, chunk)
961
1033
  end
962
1034
  end
963
1035
 
@@ -973,13 +1045,13 @@ module Raptor
973
1045
  # @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
974
1046
  def write_multiple_chunks(socket, response, body_array, use_chunked)
975
1047
  if use_chunked
976
- socket_write(socket, response)
1048
+ Request.socket_write(socket, response)
977
1049
  body_array.each do |chunk|
978
1050
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
979
1051
 
980
1052
  next if chunk.empty?
981
1053
 
982
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1054
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
983
1055
  end
984
1056
  else
985
1057
  body_array.each do |chunk|
@@ -987,7 +1059,7 @@ module Raptor
987
1059
 
988
1060
  response << chunk
989
1061
  end
990
- socket_write(socket, response)
1062
+ Request.socket_write(socket, response)
991
1063
  end
992
1064
  end
993
1065
 
@@ -1003,13 +1075,13 @@ module Raptor
1003
1075
  # @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
1004
1076
  def write_enumerable_body(socket, response, body, use_chunked)
1005
1077
  if use_chunked
1006
- socket_write(socket, response)
1078
+ Request.socket_write(socket, response)
1007
1079
  body.each do |chunk|
1008
1080
  raise TypeError, "body must yield String values" unless chunk.is_a?(String)
1009
1081
 
1010
1082
  next if chunk.empty?
1011
1083
 
1012
- socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1084
+ Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
1013
1085
  end
1014
1086
  else
1015
1087
  body.each do |chunk|
@@ -1017,7 +1089,7 @@ module Raptor
1017
1089
 
1018
1090
  response << chunk
1019
1091
  end
1020
- socket_write(socket, response)
1092
+ Request.socket_write(socket, response)
1021
1093
  end
1022
1094
  end
1023
1095
 
@@ -1090,33 +1162,6 @@ module Raptor
1090
1162
  end
1091
1163
  end
1092
1164
 
1093
- # Writes a string to the socket, retrying on partial writes and flow control blocks.
1094
- #
1095
- # Uses write_nonblock with a 5-second writable timeout to avoid blocking the
1096
- # thread indefinitely on slow clients.
1097
- #
1098
- # @param socket [TCPSocket] the socket to write to
1099
- # @param string [String] the data to write
1100
- # @return [void]
1101
- # @raise [WriteError] if the socket is not writable within the timeout or raises IOError
1102
- #
1103
- # @rbs (TCPSocket socket, String string) -> void
1104
- def socket_write(socket, string)
1105
- bytes = 0
1106
- byte_size = string.bytesize
1107
-
1108
- while bytes < byte_size
1109
- begin
1110
- bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
1111
- rescue IO::WaitWritable
1112
- raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
1113
- retry
1114
- rescue IOError
1115
- raise WriteError
1116
- end
1117
- end
1118
- end
1119
-
1120
1165
  if Socket.const_defined?(:TCP_CORK)
1121
1166
  # Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
1122
1167
  #