raptor 0.20.2 → 0.22.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.
@@ -2,13 +2,21 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nio"
5
+ require "openssl"
5
6
  require "red-black-tree"
7
+ require "timeout"
8
+
9
+ require "atomic-ruby/atomic_queue"
10
+ require "atomic-ruby/atom"
11
+ require "atomic-ruby/atomic_condition_variable"
12
+ require "atomic-ruby/atomic_count_down_latch"
13
+
14
+ require_relative "detached_body"
6
15
 
7
16
  module Raptor
8
- # Multiplexes client connections and closes them when they overrun
9
- # their per-phase timeouts (first data, subsequent chunks, and
10
- # keep-alive idle). Feeds ready bytes to the parser pipeline and hands
11
- # completed requests back to the caller-provided handlers.
17
+ # Multiplexes client connections, manages connection deadlines, feeds
18
+ # ready bytes to the parser pipeline, and hands completed requests back
19
+ # to the caller-provided handlers.
12
20
  #
13
21
  class Reactor
14
22
  # A client connection node ordered by absolute expiry time so the
@@ -50,7 +58,117 @@ module Raptor
50
58
  end
51
59
  end
52
60
 
61
+ # Tracks reactor-owned output state for a connection.
62
+ #
63
+ class ConnectionIO
64
+ # @rbs attr_accessor offset: Integer
65
+ attr_accessor :offset
66
+
67
+ # @rbs attr_accessor wait: Symbol?
68
+ attr_accessor :wait
69
+
70
+ # @rbs attr_accessor reading: bool
71
+ attr_accessor :reading
72
+
73
+ # @rbs attr_accessor closing: bool
74
+ attr_accessor :closing
75
+
76
+ # @rbs attr_accessor monitor: NIO::Monitor?
77
+ attr_accessor :monitor
78
+
79
+ # @rbs attr_accessor timeout: TimeoutClient?
80
+ attr_accessor :timeout
81
+
82
+ # @rbs attr_reader output: Array[String]
83
+ attr_reader :output
84
+
85
+ # @rbs () -> void
86
+ def initialize
87
+ @output = []
88
+ @offset = 0
89
+ @wait = nil
90
+ @reading = false
91
+ @closing = false
92
+ @monitor = nil
93
+ @timeout = nil
94
+ end
95
+
96
+ # @rbs () -> bool
97
+ def empty?
98
+ output.empty?
99
+ end
100
+ end
101
+
102
+ # Tracks reactor-owned I/O state for an HTTP/1.x detached response.
103
+ #
104
+ class Http1IO < ConnectionIO
105
+ # @rbs attr_accessor body: DetachedBody?
106
+ attr_accessor :body
107
+
108
+ # @rbs attr_accessor finishing: bool
109
+ attr_accessor :finishing
110
+
111
+ # @rbs attr_reader budget: DetachedBody::Budget
112
+ attr_reader :budget
113
+
114
+ # @rbs attr_reader input: String
115
+ attr_reader :input
116
+
117
+ # @rbs attr_reader state: Hash[Symbol, untyped]
118
+ attr_reader :state
119
+
120
+ # @rbs (DetachedBody body, Hash[Symbol, untyped] state) -> void
121
+ def initialize(body, state)
122
+ super()
123
+ @body = body
124
+ @state = state
125
+ @budget = DetachedBody::Budget.new(DETACHED_CONNECTION_BUFFER_SIZE)
126
+ @input = String.new(encoding: Encoding::ASCII_8BIT)
127
+ @reading = true
128
+ @finishing = false
129
+ end
130
+
131
+ # @rbs () -> bool
132
+ def empty?
133
+ super && !body
134
+ end
135
+ end
136
+
137
+ # Tracks reactor-owned I/O state for an HTTP/2 connection.
138
+ #
139
+ class Http2IO < ConnectionIO
140
+ # @rbs attr_reader detached: Hash[Integer, DetachedBody]
141
+ attr_reader :detached
142
+
143
+ # @rbs attr_reader ready: Array[Integer]
144
+ attr_reader :ready
145
+
146
+ # @rbs attr_reader ready_streams: Hash[Integer, bool]
147
+ attr_reader :ready_streams
148
+
149
+ # @rbs attr_reader budget: DetachedBody::Budget
150
+ attr_reader :budget
151
+
152
+ # @rbs () -> void
153
+ def initialize
154
+ super
155
+ @detached = {}
156
+ @ready = []
157
+ @ready_streams = {}
158
+ @budget = DetachedBody::Budget.new(DETACHED_CONNECTION_BUFFER_SIZE)
159
+ end
160
+
161
+ # @rbs () -> bool
162
+ def empty?
163
+ super && detached.empty?
164
+ end
165
+ end
166
+
53
167
  CHUNK_SIZE = 64 * 1024
168
+ DETACHED_CONNECTION_BUFFER_SIZE = 1024 * 1024
169
+ DETACHED_MAX_CHUNKS = 8
170
+ DETACHED_MAX_FRAMES = 8
171
+ DETACHED_WORKER_BUFFER_SIZE = 16 * 1024 * 1024
54
172
  TIMEOUT_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
55
173
 
56
174
  # @rbs @thread_pool: untyped
@@ -59,14 +177,26 @@ module Raptor
59
177
  # @rbs @first_data_timeout: Integer
60
178
  # @rbs @chunk_data_timeout: Integer
61
179
  # @rbs @persistent_data_timeout: Integer
180
+ # @rbs @write_timeout: Integer
181
+ # @rbs @http2_keepalive_interval: Integer
182
+ # @rbs @http2_keepalive_timeout: Integer
62
183
  # @rbs @selector: NIO::Selector
63
184
  # @rbs @queue: Queue[TCPSocket]
185
+ # @rbs @io_queue: AtomicQueue
64
186
  # @rbs @timeouts: RedBlackTree[TimeoutClient]
65
187
  # @rbs @id_to_socket: Hash[Integer, TCPSocket]
66
188
  # @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
67
189
  # @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
68
190
  # @rbs @id_to_writer: Hash[Integer, untyped]
69
191
  # @rbs @id_to_flow_control: Hash[Integer, untyped]
192
+ # @rbs @id_to_http2_last_stream: Hash[Integer, Integer]
193
+ # @rbs @id_to_http2_drain_stream: Hash[Integer, Integer]
194
+ # @rbs @id_to_http2_keepalive: Hash[Integer, Hash[Symbol, untyped]]
195
+ # @rbs @id_to_io: Hash[Integer, ConnectionIO]
196
+ # @rbs @detached_budget: DetachedBody::Budget
197
+ # @rbs @detached_count: Atom
198
+ # @rbs @detached_drained: AtomicConditionVariable
199
+ # @rbs @thread: Thread?
70
200
 
71
201
  # Creates a new Reactor instance.
72
202
  #
@@ -78,19 +208,26 @@ module Raptor
78
208
  # @option connection_options [Integer] :chunk_data_timeout timeout for subsequent chunks
79
209
  # @param http1_options [Hash] HTTP/1.1-specific configuration
80
210
  # @option http1_options [Integer] :persistent_data_timeout timeout for keep-alive idle connections
211
+ # @param http2_options [Hash] HTTP/2-specific configuration
212
+ # @option http2_options [Integer] :keepalive_interval idle time before sending a PING
213
+ # @option http2_options [Integer] :keepalive_timeout time to await a PING acknowledgement
81
214
  # @return [void]
82
215
  #
83
- # @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped]) -> void
84
- def initialize(http1_ractor_pool, http2_ractor_pool, thread_pool, connection_options:, http1_options:)
216
+ # @rbs (untyped http1_ractor_pool, untyped http2_ractor_pool, untyped thread_pool, connection_options: Hash[Symbol, untyped], http1_options: Hash[Symbol, untyped], http2_options: Hash[Symbol, untyped]) -> void
217
+ def initialize(http1_ractor_pool, http2_ractor_pool, thread_pool, connection_options:, http1_options:, http2_options:)
85
218
  @http1_ractor_pool = http1_ractor_pool
86
219
  @http2_ractor_pool = http2_ractor_pool
87
220
  @thread_pool = thread_pool
88
221
  @first_data_timeout = connection_options[:first_data_timeout]
89
222
  @chunk_data_timeout = connection_options[:chunk_data_timeout]
223
+ @write_timeout = connection_options[:write_timeout]
90
224
  @persistent_data_timeout = http1_options[:persistent_data_timeout]
225
+ @http2_keepalive_interval = http2_options[:keepalive_interval]
226
+ @http2_keepalive_timeout = http2_options[:keepalive_timeout]
91
227
 
92
228
  @selector = NIO::Selector.new
93
229
  @queue = Queue.new
230
+ @io_queue = AtomicQueue.new
94
231
  @timeouts = RedBlackTree.new
95
232
 
96
233
  @id_to_socket = {}
@@ -98,10 +235,18 @@ module Raptor
98
235
  @id_to_timeout = {}
99
236
  @id_to_writer = {}
100
237
  @id_to_flow_control = {}
238
+ @id_to_http2_last_stream = {}
239
+ @id_to_http2_drain_stream = {}
240
+ @id_to_http2_keepalive = {}
241
+ @id_to_io = {}
242
+ @detached_budget = DetachedBody::Budget.new(DETACHED_WORKER_BUFFER_SIZE)
243
+ @detached_count = Atom.new(0)
244
+ @detached_drained = AtomicConditionVariable.new
245
+ @thread = nil
101
246
  end
102
247
 
103
248
  # Starts the reactor's main event loop in a new thread. Runs until
104
- # the registration queue is closed and drained.
249
+ # shutdown work and pending writes are drained.
105
250
  #
106
251
  # @return [Thread] the thread running the reactor event loop
107
252
  #
@@ -109,33 +254,19 @@ module Raptor
109
254
  def run
110
255
  Thread.new do
111
256
  Thread.current.name = "Reactor"
257
+ @thread = Thread.current
112
258
 
113
- until @queue.closed? && @queue.empty?
259
+ until stopped?
114
260
  begin
115
- timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
261
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
262
+ timeout = @timeouts.min&.timeout(now)
116
263
  @selector.select(timeout) do |monitor|
117
- wakeup!(monitor.value)
264
+ handle_monitor(monitor)
118
265
  end
119
266
 
120
267
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
121
- expired = []
122
- @timeouts.traverse do |to_client|
123
- break unless to_client.timeout(now).zero?
124
-
125
- expired << to_client
126
- end
127
-
128
- expired.each do |to_client|
129
- @timeouts.delete!(to_client)
130
- id = to_client.client_data[:id]
131
- @id_to_timeout.delete(id)
132
- socket = @id_to_socket[id]
133
- next unless socket
134
-
135
- @selector.deregister(socket)
136
- socket.write(TIMEOUT_RESPONSE) rescue nil
137
- cleanup(socket)
138
- end
268
+ expire_timeouts(now)
269
+ drain_io_queue
139
270
 
140
271
  until @queue.empty?
141
272
  register(@queue.pop)
@@ -145,13 +276,19 @@ module Raptor
145
276
  end
146
277
  end
147
278
 
279
+ @id_to_flow_control.each_value(&:close)
148
280
  @id_to_socket.each_value { |socket| socket.close rescue nil }
149
281
  @id_to_socket.clear
150
282
  @socket_to_state.clear
151
283
  @id_to_timeout.clear
152
284
  @id_to_writer.clear
153
285
  @id_to_flow_control.clear
286
+ @id_to_http2_last_stream.clear
287
+ @id_to_http2_drain_stream.clear
288
+ @id_to_http2_keepalive.clear
289
+ @id_to_io.clear
154
290
  @timeouts.clear!
291
+ @thread = nil
155
292
  @selector.close
156
293
  end
157
294
  end
@@ -270,6 +407,137 @@ module Raptor
270
407
  @id_to_flow_control[id]
271
408
  end
272
409
 
410
+ # Attaches a detached response body to an HTTP/1.x connection.
411
+ #
412
+ # @rbs (TCPSocket socket, Integer id, DetachedBody body, Hash[Symbol, untyped] state, ^(Symbol) -> void finished) -> bool
413
+ def attach_http1_body(socket, id, body, state, finished)
414
+ attached = AtomicCountDownLatch.new(1)
415
+ @io_queue << [:attach_http1, id, socket, body, attached, finished, state]
416
+ @selector.wakeup rescue nil
417
+ attached.wait
418
+ return false if body.closed?
419
+
420
+ body.open
421
+ true
422
+ rescue
423
+ remove_connection(id)
424
+ raise
425
+ end
426
+
427
+ # Records an HTTP/2 stream before application dispatch, rejecting streams
428
+ # that arrived after graceful draining began.
429
+ #
430
+ # @param id [Integer] unique connection identifier
431
+ # @param stream_id [Integer] HTTP/2 stream identifier
432
+ # @return [Boolean] whether the stream may be dispatched
433
+ #
434
+ # @rbs (Integer id, Integer stream_id) -> bool
435
+ def dispatch_http2_stream(id, stream_id)
436
+ if (last_stream = @id_to_http2_drain_stream[id])
437
+ return stream_id <= last_stream
438
+ end
439
+
440
+ current = @id_to_http2_last_stream[id] || 0
441
+ @id_to_http2_last_stream[id] = stream_id if stream_id > current
442
+ true
443
+ end
444
+
445
+ # Sends GOAWAY on every HTTP/2 connection and fixes the last stream each
446
+ # connection may dispatch while existing application work drains.
447
+ #
448
+ # @yieldparam stream_id [Integer] last dispatched stream identifier
449
+ # @yieldreturn [String] serialized GOAWAY frame
450
+ # @return [void]
451
+ #
452
+ # @rbs () { (Integer stream_id) -> String } -> void
453
+ def drain_http2
454
+ @id_to_flow_control.each_key do |id|
455
+ last_stream = @id_to_http2_last_stream[id] || 0
456
+ @id_to_http2_drain_stream[id] = last_stream
457
+ writer = @id_to_writer[id]
458
+ socket = @id_to_socket[id]
459
+ writer.write_frames(socket, [yield(last_stream)]) if writer && socket
460
+ end
461
+ end
462
+
463
+ # Queues serialized HTTP/2 frames for a connection. Producers never
464
+ # write to the socket or wait for it to become writable.
465
+ #
466
+ # @param id [Integer] unique connection identifier
467
+ # @param frames [Array<String>] frame bytes to write in order
468
+ # @return [void]
469
+ #
470
+ # @rbs (Integer id, Array[String] frames) -> void
471
+ def write_http2_frames(id, frames)
472
+ data = frames.join
473
+ return if data.empty?
474
+
475
+ if Thread.current.equal?(@thread)
476
+ io = @id_to_io[id]
477
+ queue_output(io, data)
478
+ flush_output(id, io) if io && !io.wait
479
+ else
480
+ @io_queue << [:write, id, data]
481
+ @selector.wakeup rescue nil
482
+ end
483
+ end
484
+
485
+ # Attaches a detached response body to an HTTP/2 stream.
486
+ #
487
+ # @rbs (Integer id, Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> bool
488
+ def attach_http2_body(id, stream_id, body, finished)
489
+ attached = AtomicCountDownLatch.new(1)
490
+ @io_queue << [:attach_http2, id, stream_id, body, attached, finished]
491
+ @selector.wakeup rescue nil
492
+ attached.wait
493
+ return false if body.closed?
494
+
495
+ body.open
496
+ true
497
+ rescue
498
+ cancel_http2_body(id, stream_id)
499
+ raise
500
+ end
501
+
502
+ # Reconsiders detached streams after an outbound window update.
503
+ #
504
+ # @rbs (Integer id) -> void
505
+ def resume_http2_bodies(id)
506
+ return if @detached_count.value.zero?
507
+
508
+ @io_queue << [:resume_http2, id]
509
+ @selector.wakeup rescue nil
510
+ end
511
+
512
+ # Cancels a detached response stream.
513
+ #
514
+ # @rbs (Integer id, Integer stream_id) -> void
515
+ def cancel_http2_body(id, stream_id)
516
+ return if @detached_count.value.zero?
517
+
518
+ @io_queue << [:cancel_http2, id, stream_id]
519
+ @selector.wakeup rescue nil
520
+ end
521
+
522
+ # Waits for detached responses to finish, then cancels any that outlive
523
+ # the worker drain period.
524
+ #
525
+ # @rbs (Numeric timeout) -> void
526
+ def drain_detached_bodies(timeout)
527
+ barrier = AtomicCountDownLatch.new(1)
528
+ @io_queue << [:barrier, nil, barrier]
529
+ @selector.wakeup rescue nil
530
+ barrier.wait
531
+
532
+ Timeout.timeout(timeout) do
533
+ @detached_drained.wait { @detached_count.value.zero? }
534
+ end
535
+ rescue Timeout::Error
536
+ @io_queue << [:cancel_all]
537
+ @selector.wakeup rescue nil
538
+ @detached_drained.wait { @detached_count.value.zero? }
539
+ end
540
+
273
541
  # Stores an HTTP/2 connection's socket, state, writer, and flow
274
542
  # controller in the reactor's per-connection maps.
275
543
  #
@@ -278,14 +546,36 @@ module Raptor
278
546
  # @param state [Hash] initial connection state
279
547
  # @param writer [Http2::Writer] per-connection frame writer
280
548
  # @param flow_control [Http2::FlowControl] per-connection outbound flow controller
549
+ # @param ping_frame [String] serialized PING frame for keepalive probes
550
+ # @param ping_payload [String] payload expected in the corresponding acknowledgement
281
551
  # @return [void]
282
552
  #
283
- # @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped) -> void
284
- def attach_http2(id:, socket:, state:, writer:, flow_control:)
553
+ # @rbs (id: Integer, socket: TCPSocket, state: Hash[Symbol, untyped], writer: untyped, flow_control: untyped, ping_frame: String, ping_payload: String) -> void
554
+ def attach_http2(id:, socket:, state:, writer:, flow_control:, ping_frame:, ping_payload:)
285
555
  @id_to_socket[id] = socket
286
556
  @socket_to_state[socket] = state
287
557
  @id_to_writer[id] = writer
288
558
  @id_to_flow_control[id] = flow_control
559
+ @id_to_http2_last_stream[id] = 0
560
+ @id_to_io[id] = Http2IO.new
561
+ writer.attach(self, id)
562
+ if @http2_keepalive_interval.positive?
563
+ @id_to_http2_keepalive[id] = {frame: ping_frame, payload: ping_payload}
564
+ end
565
+ end
566
+
567
+ # Records acknowledgements for server keepalive probes.
568
+ #
569
+ # @param id [Integer] unique connection identifier
570
+ # @param payloads [Array<String>, nil] acknowledged PING payloads
571
+ # @return [void]
572
+ #
573
+ # @rbs (Integer id, Array[String]? payloads) -> void
574
+ def acknowledge_http2_ping(id, payloads)
575
+ keepalive = @id_to_http2_keepalive[id]
576
+ return unless keepalive && payloads&.include?(keepalive[:payload])
577
+
578
+ keepalive.delete(:deadline)
289
579
  end
290
580
 
291
581
  # Registers an attached socket for future reactor-driven reads.
@@ -322,21 +612,15 @@ module Raptor
322
612
  socket.close
323
613
  end
324
614
 
325
- # Closes the socket for the given connection and drops all reactor
326
- # state associated with it.
615
+ # Closes a connection after writing any frames already queued for it.
327
616
  #
328
617
  # @param id [Integer] unique client identifier
329
618
  # @return [void]
330
619
  #
331
620
  # @rbs (Integer id) -> void
332
621
  def close_connection(id)
333
- socket = @id_to_socket.delete(id)
334
- return unless socket
335
-
336
- @socket_to_state.delete(socket)
337
- @id_to_writer.delete(id)
338
- @id_to_flow_control.delete(id)
339
- socket.close rescue nil
622
+ @io_queue << [:close, id]
623
+ @selector.wakeup rescue nil
340
624
  end
341
625
 
342
626
  # Closes the registration queue and wakes the selector so the
@@ -362,6 +646,520 @@ module Raptor
362
646
 
363
647
  private
364
648
 
649
+ # Returns whether shutdown has no registrations or writes left to drain.
650
+ #
651
+ # @return [Boolean]
652
+ #
653
+ # @rbs () -> bool
654
+ def stopped?
655
+ @queue.closed? && @queue.empty? && @io_queue.empty? &&
656
+ @id_to_io.each_value.all?(&:empty?)
657
+ end
658
+
659
+ # Handles every expired connection deadline in order.
660
+ #
661
+ # @param now [Float] current monotonic timestamp
662
+ # @return [void]
663
+ #
664
+ # @rbs (Float now) -> void
665
+ def expire_timeouts(now)
666
+ while (client = @timeouts.min) && client.timeout(now).zero?
667
+ @timeouts.delete!(client)
668
+ id = client.client_data[:id]
669
+ if client.client_data[:write]
670
+ @id_to_io[id]&.timeout = nil
671
+ remove_connection(id)
672
+ else
673
+ @id_to_timeout.delete(id)
674
+ handle_timeout(client, now)
675
+ end
676
+ end
677
+ end
678
+
679
+ # Applies queued reactor-owned I/O operations.
680
+ #
681
+ # @return [void]
682
+ #
683
+ # @rbs () -> void
684
+ def drain_io_queue
685
+ connections = {}
686
+ while (operation = @io_queue.pop)
687
+ type, id, value, body, attached, finished, state = operation
688
+ case type
689
+ when :write
690
+ data = value
691
+ queue_output(@id_to_io[id], data)
692
+ connections[id] = true
693
+ when :close
694
+ close_after_writes(id)
695
+ when :attach_http2
696
+ begin
697
+ attach_http2_detached_body(id, value, body, finished)
698
+ ensure
699
+ attached.count_down
700
+ end
701
+ connections[id] = true
702
+ when :attach_http1
703
+ begin
704
+ attach_http1_detached_body(value, id, body, state, finished)
705
+ ensure
706
+ attached.count_down
707
+ end
708
+ when :ready_http1
709
+ flush_http1_detached_body(id)
710
+ when :ready_http2
711
+ mark_http2_detached_body_ready(id, value)
712
+ connections[id] = true
713
+ when :flush_http2
714
+ connections[id] = true
715
+ when :resume_http2
716
+ resume_http2_detached_bodies(id)
717
+ connections[id] = true
718
+ when :cancel_http2
719
+ remove_http2_detached_body(id, value, :cancelled)
720
+ when :cancel_all
721
+ cancel_detached_bodies
722
+ when :barrier
723
+ value.count_down
724
+ end
725
+ end
726
+
727
+ connections.each_key do |id|
728
+ io = @id_to_io[id]
729
+ flush_output(id, io) if io && !io.wait
730
+ flush_http2_detached_bodies(id) if io.is_a?(Http2IO) && !io.wait && !io.detached.empty?
731
+ end
732
+ end
733
+
734
+ # Adds a detached body to an HTTP/1.x connection.
735
+ #
736
+ # @rbs (TCPSocket socket, Integer id, DetachedBody body, Hash[Symbol, untyped] state, ^(Symbol) -> void finished) -> void
737
+ def attach_http1_detached_body(socket, id, body, state, finished)
738
+ io = Http1IO.new(body, state)
739
+ dispatch = proc { |callback| @thread_pool << callback }
740
+ wake = proc do
741
+ @io_queue << [:ready_http1, id]
742
+ @selector.wakeup rescue nil
743
+ end
744
+ unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
745
+ socket.close rescue nil
746
+ return
747
+ end
748
+
749
+ @id_to_socket[id] = socket
750
+ @socket_to_state[socket] = {id: id, protocol: :http1, detached: true}
751
+ @id_to_io[id] = io
752
+ @detached_count.swap { |count| count + 1 }
753
+ update_monitor(id, io)
754
+ end
755
+
756
+ # Writes buffered HTTP/1.x body chunks without blocking.
757
+ #
758
+ # @rbs (Integer id) -> void
759
+ def flush_http1_detached_body(id)
760
+ io = @id_to_io[id]
761
+ return unless io.is_a?(Http1IO) && !io.wait && io.output.empty?
762
+
763
+ DETACHED_MAX_CHUNKS.times do
764
+ size = io.body&.next_size
765
+ break unless size
766
+
767
+ if size.zero?
768
+ queue_output(io, Http1.encode_trailers(io.body.trailers)) if io.state[:chunked]
769
+ io.finishing = true
770
+ flush_output(id, io)
771
+ break
772
+ end
773
+
774
+ chunk = io.body.shift(size)
775
+ queue_output(io, io.state[:chunked] ? Http1.encode_chunk(chunk) : chunk)
776
+ flush_output(id, io)
777
+ break if io.wait
778
+ end
779
+
780
+ if !io.finishing && !io.wait && io.body&.next_size
781
+ @io_queue << [:ready_http1, id]
782
+ @selector.wakeup rescue nil
783
+ end
784
+ end
785
+
786
+ # Finishes an HTTP/1.x detached response and either reuses or closes its connection.
787
+ #
788
+ # @rbs (Integer id, Http1IO io) -> void
789
+ def finish_http1_detached_body(id, io)
790
+ socket = @id_to_socket[id]
791
+ return unless socket
792
+
793
+ body = io.body
794
+ io.body = nil
795
+ body.finish(:closed)
796
+ detached_body_removed
797
+ @id_to_io.delete(id)
798
+ deregister(socket, io)
799
+ clear_write_timeout(io)
800
+
801
+ if io.state[:keep_alive]
802
+ state = {
803
+ id: id,
804
+ request_count: io.state[:request_count],
805
+ remote_addr: io.state[:remote_addr],
806
+ url_scheme: io.state[:url_scheme],
807
+ persisted: true,
808
+ }
809
+ unless io.input.empty?
810
+ state[:buffer] = io.input
811
+ @socket_to_state[socket] = state
812
+ @http1_ractor_pool << Ractor.make_shareable(state)
813
+ return
814
+ end
815
+
816
+ persist(socket, id, io.state[:request_count], remote_addr: io.state[:remote_addr], url_scheme: io.state[:url_scheme])
817
+ else
818
+ @socket_to_state.delete(socket)
819
+ @id_to_socket.delete(id)
820
+ socket.close rescue nil
821
+ end
822
+ end
823
+
824
+ # Cancels an HTTP/1.x detached response and closes its connection.
825
+ #
826
+ # @rbs (Integer id, Symbol reason) -> void
827
+ def remove_http1_detached_body(id, reason)
828
+ io = @id_to_io[id]
829
+ return unless io.is_a?(Http1IO) && io.body
830
+
831
+ body = io.body
832
+ io.body = nil
833
+ body.finish(reason)
834
+ detached_body_removed
835
+ remove_connection(id)
836
+ end
837
+
838
+ # Adds a detached body to an HTTP/2 stream.
839
+ #
840
+ # @rbs (Integer id, Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> void
841
+ def attach_http2_detached_body(id, stream_id, body, finished)
842
+ io = @id_to_io[id]
843
+ dispatch = proc { |callback| @thread_pool << callback }
844
+ unless io
845
+ body.attach([], proc {}, dispatch, finished)
846
+ body.finish(:connection_closed)
847
+ return
848
+ end
849
+
850
+ wake = proc do
851
+ @io_queue << [:ready_http2, id, stream_id]
852
+ @selector.wakeup rescue nil
853
+ end
854
+ return unless body.attach([io.budget, @detached_budget], wake, dispatch, finished)
855
+
856
+ io.detached[stream_id] = body
857
+ @detached_count.swap { |count| count + 1 }
858
+ end
859
+
860
+ # Marks an HTTP/2 detached stream eligible for fair scheduling.
861
+ #
862
+ # @rbs (Integer id, Integer stream_id) -> void
863
+ def mark_http2_detached_body_ready(id, stream_id)
864
+ io = @id_to_io[id]
865
+ return unless io&.detached&.key?(stream_id) && !io.ready_streams.key?(stream_id)
866
+
867
+ io.ready << stream_id
868
+ io.ready_streams[stream_id] = true
869
+ end
870
+
871
+ # Marks every HTTP/2 detached stream eligible after flow-control capacity changes.
872
+ #
873
+ # @rbs (Integer id) -> void
874
+ def resume_http2_detached_bodies(id)
875
+ io = @id_to_io[id]
876
+ return unless io
877
+
878
+ io.detached.each_key { |stream_id| mark_http2_detached_body_ready(id, stream_id) }
879
+ end
880
+
881
+ # Writes HTTP/2 detached streams in round-robin order without waiting for flow control.
882
+ #
883
+ # @rbs (Integer id) -> void
884
+ def flush_http2_detached_bodies(id)
885
+ io = @id_to_io[id]
886
+ flow_control = @id_to_flow_control[id]
887
+ socket = @id_to_socket[id]
888
+ return unless io && flow_control && socket
889
+
890
+ DETACHED_MAX_FRAMES.times do
891
+ break if io.wait || !io.output.empty?
892
+
893
+ stream_id = io.ready.shift
894
+ break unless stream_id
895
+
896
+ io.ready_streams.delete(stream_id)
897
+ body = io.detached[stream_id]
898
+ next unless body
899
+
900
+ size = body.next_size
901
+ next unless size
902
+
903
+ if size.zero?
904
+ finish_http2_detached_body(id, stream_id, body)
905
+ next
906
+ end
907
+
908
+ granted = flow_control.try_acquire(stream_id, size)
909
+ next if granted.zero?
910
+
911
+ chunk = body.shift(granted)
912
+ parser = Http2Parser.new
913
+ queue_output(io, parser.build_frame(:data, 0, stream_id, chunk))
914
+ flush_output(id, io)
915
+ mark_http2_detached_body_ready(id, stream_id) if body.next_size
916
+ rescue Http2::StreamClosedError
917
+ remove_http2_detached_body(id, stream_id, :cancelled)
918
+ end
919
+
920
+ if !io.wait && io.output.empty? && !io.ready.empty?
921
+ @io_queue << [:flush_http2, id]
922
+ @selector.wakeup rescue nil
923
+ end
924
+ end
925
+
926
+ # Finishes an HTTP/2 detached stream with DATA or trailing HEADERS.
927
+ #
928
+ # @rbs (Integer id, Integer stream_id, DetachedBody body) -> void
929
+ def finish_http2_detached_body(id, stream_id, body)
930
+ parser = Http2Parser.new
931
+ frame = if body.trailers.empty?
932
+ parser.build_frame(:data, Http2::FLAG_END_STREAM, stream_id, nil)
933
+ else
934
+ encoded = parser.encode_response_trailers(body.trailers)
935
+ parser.build_frame(:headers, Http2::FLAG_END_STREAM | Http2::FLAG_END_HEADERS, stream_id, encoded)
936
+ end
937
+ io = @id_to_io[id]
938
+ queue_output(io, frame)
939
+ flush_output(id, io)
940
+ remove_http2_detached_body(id, stream_id, :closed)
941
+ end
942
+
943
+ # Removes an HTTP/2 detached stream and schedules its close callback.
944
+ #
945
+ # @rbs (Integer id, Integer stream_id, Symbol reason) -> void
946
+ def remove_http2_detached_body(id, stream_id, reason)
947
+ io = @id_to_io[id]
948
+ body = io&.detached&.delete(stream_id)
949
+ return unless body
950
+
951
+ io.ready_streams.delete(stream_id)
952
+ body.finish(reason)
953
+ detached_body_removed
954
+ end
955
+
956
+ # Cancels every detached response during worker shutdown.
957
+ #
958
+ # @rbs () -> void
959
+ def cancel_detached_bodies
960
+ @id_to_io.to_a.each do |id, io|
961
+ if io.is_a?(Http1IO)
962
+ remove_http1_detached_body(id, :shutdown)
963
+ else
964
+ io.detached.each_key.to_a.each do |stream_id|
965
+ remove_http2_detached_body(id, stream_id, :shutdown)
966
+ end
967
+ end
968
+ end
969
+ end
970
+
971
+ # Records one detached stream finishing.
972
+ #
973
+ # @rbs () -> void
974
+ def detached_body_removed
975
+ remaining = nil
976
+ @detached_count.swap do |count|
977
+ remaining = count - 1
978
+ remaining
979
+ end
980
+ @detached_drained.broadcast if remaining.zero?
981
+ end
982
+
983
+ # Closes a connection once its queued frames are written.
984
+ #
985
+ # @param id [Integer] unique client identifier
986
+ # @return [void]
987
+ #
988
+ # @rbs (Integer id) -> void
989
+ def close_after_writes(id)
990
+ io = @id_to_io[id]
991
+ return unless io
992
+
993
+ if io.empty?
994
+ remove_connection(id)
995
+ else
996
+ io.closing = true
997
+ end
998
+ end
999
+
1000
+ # Immediately closes a connection and drops its reactor state.
1001
+ #
1002
+ # @param id [Integer] unique client identifier
1003
+ # @return [void]
1004
+ #
1005
+ # @rbs (Integer id) -> void
1006
+ def remove_connection(id)
1007
+ socket = @id_to_socket[id]
1008
+ cleanup(socket) if socket
1009
+ end
1010
+
1011
+ # Adds bytes to one connection and writes as much as the socket accepts.
1012
+ #
1013
+ # @param io [ConnectionIO] connection I/O state
1014
+ # @param data [String] serialized response bytes
1015
+ # @return [void]
1016
+ #
1017
+ # @rbs (ConnectionIO? io, String data) -> void
1018
+ def queue_output(io, data)
1019
+ return unless io && !io.closing
1020
+
1021
+ if io.output.empty?
1022
+ io.output << data
1023
+ else
1024
+ io.output[-1] << data
1025
+ end
1026
+ end
1027
+
1028
+ # Drains one connection's queued response bytes without blocking.
1029
+ #
1030
+ # @param id [Integer] unique connection identifier
1031
+ # @param io [ConnectionIO] connection I/O state
1032
+ # @return [void]
1033
+ #
1034
+ # @rbs (Integer id, ConnectionIO? io) -> void
1035
+ def flush_output(id, io)
1036
+ socket = @id_to_socket[id]
1037
+ return unless socket && io
1038
+
1039
+ while (data = io.output.first)
1040
+ chunk = io.offset.zero? ? data : data.byteslice(io.offset..-1)
1041
+
1042
+ begin
1043
+ written = socket.write_nonblock(chunk)
1044
+ rescue IO::WaitReadable
1045
+ wait_for_write(id, io, :read)
1046
+ return
1047
+ rescue IO::WaitWritable
1048
+ wait_for_write(id, io, :write)
1049
+ return
1050
+ rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
1051
+ remove_connection(id)
1052
+ return
1053
+ end
1054
+
1055
+ if written.zero?
1056
+ wait_for_write(id, io, :write)
1057
+ return
1058
+ end
1059
+
1060
+ clear_write_timeout(io)
1061
+ io.offset += written
1062
+ if io.offset == data.bytesize
1063
+ io.output.shift
1064
+ io.offset = 0
1065
+ end
1066
+ end
1067
+
1068
+ io.wait = nil
1069
+ if io.is_a?(Http1IO) && io.finishing
1070
+ finish_http1_detached_body(id, io)
1071
+ elsif io.closing
1072
+ remove_connection(id)
1073
+ else
1074
+ update_monitor(id, io)
1075
+ end
1076
+ end
1077
+
1078
+ # Registers the readiness needed to continue a partial socket write.
1079
+ #
1080
+ # @param id [Integer] unique connection identifier
1081
+ # @param io [ConnectionIO] connection I/O state
1082
+ # @param readiness [Symbol] `:read` or `:write`
1083
+ # @return [void]
1084
+ #
1085
+ # @rbs (Integer id, ConnectionIO io, Symbol readiness) -> void
1086
+ def wait_for_write(id, io, readiness)
1087
+ io.wait = readiness
1088
+ track_write_timeout(id, io)
1089
+ update_monitor(id, io)
1090
+ end
1091
+
1092
+ # Updates selector interest for one reactor-owned connection.
1093
+ #
1094
+ # @param id [Integer] unique connection identifier
1095
+ # @param io [ConnectionIO] connection I/O state
1096
+ # @return [void]
1097
+ #
1098
+ # @rbs (Integer id, ConnectionIO io) -> void
1099
+ def update_monitor(id, io)
1100
+ socket = @id_to_socket[id]
1101
+ return unless socket && io
1102
+
1103
+ read = io.reading || io.wait == :read
1104
+ write = io.wait == :write
1105
+ interests = if read && write
1106
+ :rw
1107
+ elsif read
1108
+ :r
1109
+ elsif write
1110
+ :w
1111
+ end
1112
+
1113
+ if io.monitor
1114
+ io.monitor.interests = interests
1115
+ elsif interests
1116
+ io.monitor = @selector.register(socket, interests)
1117
+ io.monitor.value = socket
1118
+ end
1119
+ end
1120
+
1121
+ # Starts or refreshes one connection's non-blocking write deadline.
1122
+ #
1123
+ # @param id [Integer] unique connection identifier
1124
+ # @param io [ConnectionIO] connection I/O state
1125
+ # @return [void]
1126
+ #
1127
+ # @rbs (Integer id, ConnectionIO io) -> void
1128
+ def track_write_timeout(id, io)
1129
+ clear_write_timeout(io)
1130
+ client = TimeoutClient.new({id: id, write: true})
1131
+ client.timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @write_timeout
1132
+ @timeouts << client
1133
+ io.timeout = client
1134
+ end
1135
+
1136
+ # Removes one connection's non-blocking write deadline.
1137
+ #
1138
+ # @param io [ConnectionIO, nil] connection I/O state
1139
+ # @return [void]
1140
+ #
1141
+ # @rbs (ConnectionIO? io) -> void
1142
+ def clear_write_timeout(io)
1143
+ client = io&.timeout
1144
+ @timeouts.delete!(client) if client
1145
+ io.timeout = nil if io
1146
+ end
1147
+
1148
+ # Removes a socket from selector tracking.
1149
+ #
1150
+ # @param socket [TCPSocket] socket to deregister
1151
+ # @param io [ConnectionIO, nil] connection I/O state
1152
+ # @return [void]
1153
+ #
1154
+ # @rbs (TCPSocket socket, ConnectionIO? io) -> void
1155
+ def deregister(socket, io)
1156
+ return unless io&.monitor || @selector.registered?(socket)
1157
+
1158
+ @selector.deregister(socket)
1159
+ io.monitor = nil if io
1160
+ rescue IOError
1161
+ end
1162
+
365
1163
  # Registers a socket with the NIO selector and sets up timeout tracking.
366
1164
  #
367
1165
  # @param socket [TCPSocket] the socket to register
@@ -369,22 +1167,146 @@ module Raptor
369
1167
  #
370
1168
  # @rbs (TCPSocket socket) -> void
371
1169
  def register(socket)
372
- @selector.register(socket, :r).value = socket
373
-
374
1170
  state = @socket_to_state[socket]
375
- client = TimeoutClient.new(state)
376
- timeout = if state[:persisted]
377
- @persistent_data_timeout
1171
+ return unless state
1172
+
1173
+ if state[:protocol] == :http2
1174
+ io = @id_to_io[state[:id]]
1175
+ io.reading = true
1176
+ update_monitor(state[:id], io)
1177
+ else
1178
+ @selector.register(socket, :r).value = socket
1179
+ end
1180
+
1181
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1182
+ timeout_at = if state[:protocol] == :http2
1183
+ if state[:http2_preface_received]
1184
+ keepalive = @id_to_http2_keepalive[state[:id]]
1185
+ keepalive && (keepalive[:deadline] || now + @http2_keepalive_interval)
1186
+ else
1187
+ now + @first_data_timeout
1188
+ end
1189
+ elsif state[:persisted]
1190
+ now + @persistent_data_timeout
378
1191
  elsif first_data_received?(state)
379
- @chunk_data_timeout
1192
+ now + @chunk_data_timeout
380
1193
  else
381
- @first_data_timeout
1194
+ now + @first_data_timeout
382
1195
  end
383
- client.timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
1196
+ track_timeout(state, timeout_at) if timeout_at
1197
+ end
1198
+
1199
+ # Adds a connection deadline to the timeout tree.
1200
+ #
1201
+ # @param state [Hash] client connection state
1202
+ # @param timeout_at [Float] absolute monotonic expiry time
1203
+ # @return [void]
1204
+ #
1205
+ # @rbs (Hash[Symbol, untyped] state, Float timeout_at) -> void
1206
+ def track_timeout(state, timeout_at)
1207
+ client = TimeoutClient.new(state)
1208
+ client.timeout_at = timeout_at
384
1209
  @timeouts << client
385
1210
  @id_to_timeout[state[:id]] = client
386
1211
  end
387
1212
 
1213
+ # Handles an expired connection deadline, sending an HTTP/2 keepalive
1214
+ # probe before closing the connection when its acknowledgement is late.
1215
+ #
1216
+ # @param client [TimeoutClient] expired client
1217
+ # @param now [Float] current monotonic timestamp
1218
+ # @return [void]
1219
+ #
1220
+ # @rbs (TimeoutClient client, Float now) -> void
1221
+ def handle_timeout(client, now)
1222
+ state = client.client_data
1223
+ id = state[:id]
1224
+ socket = @id_to_socket[id]
1225
+ return unless socket
1226
+
1227
+ keepalive = @id_to_http2_keepalive[id] if state[:http2_preface_received]
1228
+ if keepalive && !keepalive[:deadline]
1229
+ keepalive[:deadline] = now + @http2_keepalive_timeout
1230
+ @id_to_writer[id].write_frames(socket, [keepalive[:frame]])
1231
+ track_timeout(state, keepalive[:deadline])
1232
+ return
1233
+ end
1234
+
1235
+ socket.write(TIMEOUT_RESPONSE) rescue nil unless state[:protocol] == :http2
1236
+ cleanup(socket)
1237
+ end
1238
+
1239
+ # Dispatches readable and writable events without allowing application
1240
+ # threads to take ownership of HTTP/2 sockets.
1241
+ #
1242
+ # @param monitor [NIO::Monitor] ready selector monitor
1243
+ # @return [void]
1244
+ #
1245
+ # @rbs (NIO::Monitor monitor) -> void
1246
+ def handle_monitor(monitor)
1247
+ socket = monitor.value
1248
+ state = @socket_to_state[socket]
1249
+ return unless state
1250
+
1251
+ if state[:detached]
1252
+ handle_http1_detached_monitor(monitor, state[:id])
1253
+ return
1254
+ end
1255
+
1256
+ unless state[:protocol] == :http2
1257
+ wakeup!(socket)
1258
+ return
1259
+ end
1260
+
1261
+ id = state[:id]
1262
+ io = @id_to_io[id]
1263
+ return unless io
1264
+
1265
+ wait = io.wait
1266
+ if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
1267
+ flush_output(id, io)
1268
+ return unless @id_to_socket.key?(id)
1269
+ return if io.wait == :read
1270
+ flush_http2_detached_bodies(id) unless io.detached.empty?
1271
+ end
1272
+
1273
+ return unless io.reading && monitor.readable?
1274
+
1275
+ io.reading = false
1276
+ update_monitor(id, io)
1277
+ to_client = @id_to_timeout.delete(id)
1278
+ @timeouts.delete!(to_client) if to_client
1279
+ read_and_queue_for_parse(socket, state)
1280
+ end
1281
+
1282
+ # Handles readiness for an HTTP/1.x detached response.
1283
+ #
1284
+ # @rbs (NIO::Monitor monitor, Integer id) -> void
1285
+ def handle_http1_detached_monitor(monitor, id)
1286
+ socket = monitor.value
1287
+ io = @id_to_io[id]
1288
+ return unless io.is_a?(Http1IO)
1289
+
1290
+ wait = io.wait
1291
+ if (wait == :read && monitor.readable?) || (wait == :write && monitor.writable?)
1292
+ flush_output(id, io)
1293
+ return unless @id_to_io.key?(id)
1294
+
1295
+ flush_http1_detached_body(id) unless io.wait
1296
+ end
1297
+
1298
+ return unless io.reading && monitor.readable?
1299
+
1300
+ io.input << socket.read_nonblock(CHUNK_SIZE)
1301
+ io.reading = false
1302
+ update_monitor(id, io)
1303
+ rescue IO::WaitReadable
1304
+ rescue IO::WaitWritable
1305
+ wait_for_write(id, io, :write)
1306
+ rescue EOFError, IOError, SystemCallError, OpenSSL::SSL::SSLError
1307
+ remove_connection(id)
1308
+ end
1309
+
388
1310
  # Handles socket wakeup by deregistering and queuing for processing.
389
1311
  #
390
1312
  # @param socket [TCPSocket] the socket that became ready
@@ -395,7 +1317,7 @@ module Raptor
395
1317
  @selector.deregister(socket)
396
1318
  state = @socket_to_state[socket]
397
1319
  to_client = @id_to_timeout.delete(state[:id])
398
- @timeouts.delete!(to_client)
1320
+ @timeouts.delete!(to_client) if to_client
399
1321
  read_and_queue_for_parse(socket, state)
400
1322
  end
401
1323
 
@@ -439,10 +1361,30 @@ module Raptor
439
1361
  # @rbs (TCPSocket socket) -> void
440
1362
  def cleanup(socket)
441
1363
  state = @socket_to_state.delete(socket)
1364
+ io = @id_to_io.delete(state[:id])
1365
+ deregister(socket, io)
442
1366
  @id_to_socket.delete(state[:id])
443
1367
  @id_to_writer.delete(state[:id])
444
- @id_to_flow_control.delete(state[:id])
445
- socket.close
1368
+ @id_to_flow_control.delete(state[:id])&.close
1369
+ @id_to_http2_last_stream.delete(state[:id])
1370
+ @id_to_http2_drain_stream.delete(state[:id])
1371
+ @id_to_http2_keepalive.delete(state[:id])
1372
+ if io.is_a?(Http1IO)
1373
+ if io.body
1374
+ io.body.finish(:connection_closed)
1375
+ io.body = nil
1376
+ detached_body_removed
1377
+ end
1378
+ elsif io
1379
+ io.detached.each_value do |body|
1380
+ body.finish(:connection_closed)
1381
+ detached_body_removed
1382
+ end
1383
+ end
1384
+ client = @id_to_timeout.delete(state[:id])
1385
+ @timeouts.delete!(client) if client
1386
+ clear_write_timeout(io)
1387
+ socket.close rescue nil
446
1388
  end
447
1389
 
448
1390
  # Returns true when the request has been fully parsed.