http3 0.0.3 → 0.0.5

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.
data/lib/http3/server.rb CHANGED
@@ -19,10 +19,29 @@ module HTTP3
19
19
  #
20
20
  # Only failures are logged, one line each: the event, then key=value
21
21
  # pairs. Requests are not logged; use Rack::CommonLogger for that. Query
22
- # strings and header values never appear in the log.
22
+ # strings and header values never appear in the log. Those answered
23
+ # without the application (400, 408, 413, 431, 501, 503), and the
24
+ # responses aborted after response_timeout, are counted instead:
25
+ # refusal_log_interval seconds after the first of them, and when #run
26
+ # ends, a warn line per status tells how many there were since, such as
27
+ # "requests_refused status=503 count=1234 seconds=60", followed by
28
+ # "responses_timed_out count=56 seconds=60" if any were aborted.
29
+ #
30
+ # One thread drives every connection; the application runs on worker
31
+ # threads (WorkerPool), and only there: its call, the each of its body
32
+ # and the close of its body all run on the worker that took the request.
33
+ # The workers hand their responses to that thread through a queue, and
34
+ # wake it through a pipe.
23
35
  class Server
24
36
  ALPN = ["h3"].freeze
25
37
 
38
+ # A request handed to a worker. remote is the client's address, taken
39
+ # when the request was handed out, for the log; pipe is the response's
40
+ # Body::Pipe, once the worker has one; started is true once a worker
41
+ # has taken the job.
42
+ Job = Struct.new(:conn, :session, :request, :remote, :pipe, :started)
43
+ private_constant :Job
44
+
26
45
  # What the signal handlers write to the self-pipe.
27
46
  SIGNALS = {"TERM" => "T", "INT" => "I", "HUP" => "H"}.freeze
28
47
 
@@ -42,12 +61,54 @@ module HTTP3
42
61
  # certificate_path is a PEM chain, leaf first; private_key_path a PEM key
43
62
  # without passphrase. qlog_dir writes one qlog file per connection.
44
63
  # max_request_body and max_field_section_size (bytes) bound a request's
45
- # body (413 beyond) and header section (431 beyond); send_buffer_size
46
- # bounds the bytes of a response body queued but unsent per stream.
64
+ # body (413 beyond) and header section (431 beyond). A body over
65
+ # request_body_buffer_size bytes is written to a temporary file in
66
+ # Dir.tmpdir. max_connection_request_buffer bounds the bytes of memory
67
+ # the requests of a connection hold until the application is done with
68
+ # them, and max_total_request_buffer those of every connection;
69
+ # max_connection_request_spool and max_total_request_spool bound the
70
+ # bytes in temporary files, and max_connection_request_spool_files and
71
+ # max_total_request_spool_files the files open at once. A request that
72
+ # would pass any of them, or whose file cannot be made or written, gets
73
+ # 503. They have to leave room for a request at the limits on a server
74
+ # with nothing else under way, or ArgumentError is raised; and when
75
+ # bodies may go to files, Dir.tmpdir has to take one, or
76
+ # SystemCallError is. send_buffer_size
77
+ # bounds the bytes of a response body queued but unsent per stream,
78
+ # max_connection_send_buffer those of the array, each and streaming
79
+ # bodies of a connection, and max_total_send_buffer those of every
80
+ # connection; a body waits for room. Each has to be at least the one before. A request
81
+ # not received in full within request_timeout seconds of the start of
82
+ # its headers gets 408, and one that waits that long for a worker
83
+ # afterwards 503; nil is no limit. A response that stalls, waiting on
84
+ # its client with nothing of it acknowledged, for response_timeout
85
+ # seconds is aborted with H3_REQUEST_CANCELLED, and a worker writing it
86
+ # gets StreamClosed; it bounds a stall, not the whole response. nil is
87
+ # no limit, and unless either is nil, it has to be less than
88
+ # request_timeout. refusal_log_interval is the seconds between the
89
+ # requests_refused and responses_timed_out lines (see above). threads
90
+ # is the number of worker threads.
47
91
  def initialize(app:, bind:, certificate_path:, private_key_path:, transport_params: nil, settings: nil,
48
- qlog_dir: nil, max_connections: 1024, max_request_body: 1_048_576, max_field_section_size: 65_536,
49
- send_buffer_size: 65_536, shutdown_timeout: 30, logger: Logger.new($stderr))
50
- @adapter = RackAdapter.new(app)
92
+ qlog_dir: nil, max_connections: 1024, threads: 3, max_request_body: 1_048_576, max_field_section_size: 65_536,
93
+ max_connection_request_buffer: 4_194_304, max_total_request_buffer: 67_108_864,
94
+ request_body_buffer_size: 65_536, max_connection_request_spool: 16_777_216,
95
+ max_total_request_spool: 1_073_741_824, max_connection_request_spool_files: 16,
96
+ max_total_request_spool_files: 256,
97
+ send_buffer_size: 65_536, max_connection_send_buffer: 1_048_576, max_total_send_buffer: 67_108_864,
98
+ request_timeout: 30, response_timeout: 10, refusal_log_interval: 60, shutdown_timeout: 30,
99
+ logger: Logger.new($stderr))
100
+ check_request_limits(max_request_body:, max_field_section_size:, max_connection_request_buffer:,
101
+ max_total_request_buffer:, request_body_buffer_size:, max_connection_request_spool:,
102
+ max_total_request_spool:, max_connection_request_spool_files:, max_total_request_spool_files:)
103
+ # Found out now rather than by the first large request.
104
+ Session.spool_file.close if max_request_body > request_body_buffer_size
105
+ check_send_limits(send_buffer_size, max_connection_send_buffer, max_total_send_buffer)
106
+ check_threads(threads)
107
+ check_request_timeout(request_timeout)
108
+ check_response_timeout(response_timeout, request_timeout)
109
+ check_refusal_log_interval(refusal_log_interval)
110
+ @app = app
111
+ @threads = threads
51
112
  @bind_address = parse_bind(bind)
52
113
  @certificate_path = certificate_path
53
114
  @private_key_path = private_key_path
@@ -60,7 +121,19 @@ module HTTP3
60
121
  @max_connections = max_connections
61
122
  @max_request_body = max_request_body
62
123
  @max_field_section_size = max_field_section_size
124
+ @max_connection_request_buffer = max_connection_request_buffer
125
+ @max_total_request_buffer = max_total_request_buffer
126
+ @request_body_buffer_size = request_body_buffer_size
127
+ @max_connection_request_spool = max_connection_request_spool
128
+ @max_total_request_spool = max_total_request_spool
129
+ @max_connection_request_spool_files = max_connection_request_spool_files
130
+ @max_total_request_spool_files = max_total_request_spool_files
63
131
  @send_buffer_size = send_buffer_size
132
+ @max_connection_send_buffer = max_connection_send_buffer
133
+ @max_total_send_buffer = max_total_send_buffer
134
+ @request_timeout = request_timeout
135
+ @response_timeout = response_timeout
136
+ @refusal_log_interval = refusal_log_interval
64
137
  @shutdown_timeout = shutdown_timeout
65
138
  @logger = logger
66
139
  end
@@ -68,6 +141,17 @@ module HTTP3
68
141
  # Serve until told to stop (see the class comment). Returns nil.
69
142
  def run
70
143
  sig_r, sig_w = IO.pipe
144
+ # The workers write to it when they have a result for the loop, or a
145
+ # Body::Pipe has something to take.
146
+ @wake_r, @wake_w = IO.pipe
147
+ @results = Thread::Queue.new
148
+ # The jobs handed to a worker and not done yet, and whether the
149
+ # workers have been told to stop; the lock is for the pipes of the
150
+ # jobs, which workers set.
151
+ @jobs = {}.compare_by_identity
152
+ @jobs_lock = Thread::Mutex.new
153
+ @workers_stopped = false
154
+ @adapter = RackAdapter.new(@app, pipe_capacity: @send_buffer_size, wake: method(:wake))
71
155
  @sock = UDPSocket.new(@bind_address.afamily)
72
156
  @sock.bind(@bind_address.ip_address, @bind_address.ip_port)
73
157
  @local_address = @sock.local_address
@@ -77,31 +161,59 @@ module HTTP3
77
161
  )
78
162
  @sessions = {}
79
163
  @remotes = {}
164
+ # The index in @sessions of the connection whose turn it is to hand a
165
+ # request to the application (#next_request).
166
+ @next_turn = 0
167
+ @request_buffer = Session.shared_buffer(@max_total_request_buffer)
168
+ @request_spool = Session.shared_spool(@max_total_request_spool, files: @max_total_request_spool_files)
169
+ # When request_spool_failed was last logged; nil until then.
170
+ @spool_failure_logged_at = nil
80
171
  @blocked_send = nil
81
172
  @phase = :serving
82
173
  @drain_deadline = nil
174
+ # {status => count} of the requests answered without the
175
+ # application, since @refusals_since.
176
+ @refusals = Hash.new(0)
177
+ # The responses aborted after response_timeout, since @refusals_since.
178
+ @responses_timed_out = 0
179
+ @refusals_since = nil
83
180
  old_traps = install_traps(sig_w)
181
+ @pool = WorkerPool.new(@threads).start
84
182
 
85
183
  turn(sig_r) until finished?
86
184
  close_connections
185
+ stop_workers
186
+ log_refusals
87
187
  nil
88
188
  ensure
89
189
  restore_traps(old_traps) if old_traps
190
+ stop_workers if @pool && !@workers_stopped
90
191
  @sessions&.each_value { |session| release_session(session) }
91
- [@sock, sig_r, sig_w].each { |io| io.close if io && !io.closed? }
192
+ [@sock, sig_r, sig_w, @wake_r, @wake_w].each { |io| io.close if io && !io.closed? }
92
193
  end
93
194
 
94
195
  private
95
196
 
96
- # One turn of the loop.
197
+ # One turn of the loop. The room left in max_total_send_buffer is
198
+ # counted once, at the start. Only fill_bodies queues more, so until
199
+ # then the room can only have grown (nghttp3 taking what flush sends),
200
+ # and filling by the room counted queues no more than the limit allows.
97
201
  def turn(sig_r)
202
+ room = send_room
98
203
  writers = @blocked_send ? [@sock] : nil
99
- readable, writable, = IO.select([@sock, sig_r], writers, nil, select_timeout)
204
+ readable, writable, = IO.select([@sock, sig_r, @wake_r], writers, nil, select_timeout(room))
100
205
  handle_signals(sig_r) if readable&.include?(sig_r)
101
206
  flush if writable&.include?(@sock)
102
207
  receive if readable&.include?(@sock)
208
+ # Emptied before the results are taken: a result put after that
209
+ # leaves a byte for the next select.
210
+ drain_wake
211
+ take_results
212
+ expire_requests
213
+ expire_responses
103
214
  serve_requests
104
- fill_bodies
215
+ log_refusals if refusals_due?
216
+ fill_bodies(room)
105
217
  @endpoint.handle_expiry if due?(@endpoint.expiry)
106
218
  flush
107
219
  reap
@@ -110,19 +222,31 @@ module HTTP3
110
222
  def finished?
111
223
  case @phase
112
224
  when :serving then false
113
- when :draining then now >= @drain_deadline || @sessions.each_value.none?(&:busy?)
225
+ when :draining then now >= @drain_deadline || (@jobs.empty? && @sessions.each_value.none?(&:busy?))
114
226
  else true
115
227
  end
116
228
  end
117
229
 
118
- # Until the next timer or the end of the drain, but no wait at all while
119
- # a body could take more.
120
- def select_timeout
121
- return 0 if @sessions.each_value.any? { |session| session.wants_fill?(@send_buffer_size) }
230
+ # Until the next timer, the next request (coming in, or waiting for a
231
+ # worker) or response (waiting on its client) to run out of time, the
232
+ # next requests_refused and responses_timed_out lines or the end of the
233
+ # drain, but no wait at all while a body could take more within the
234
+ # send buffers: a Body::Pipe only once its worker has written to it. A
235
+ # worker with a result wakes the loop through @wake_r.
236
+ def select_timeout(room)
237
+ wants_fill = @sessions.each_value.any? { |session|
238
+ session.wants_fill?(@send_buffer_size, connection_limit: @max_connection_send_buffer, room: room)
239
+ }
240
+ return 0 if wants_fill
122
241
 
123
242
  timeouts = []
124
243
  expiry = @endpoint.expiry
125
244
  timeouts << [(expiry - now_ns) / 1e9, 0.0].max if expiry
245
+ deadline = @sessions.each_value.filter_map(&:next_request_deadline).min
246
+ timeouts << [deadline - now, 0.0].max if deadline
247
+ deadline = @sessions.each_value.filter_map(&:next_response_deadline).min
248
+ timeouts << [deadline - now, 0.0].max if deadline
249
+ timeouts << [refusal_log_at - now, 0.0].max if refusal_log_at
126
250
  timeouts << [@drain_deadline - now, 0.0].max if @drain_deadline
127
251
  timeouts.min
128
252
  end
@@ -142,40 +266,271 @@ module HTTP3
142
266
  conn = @endpoint.read_pkt(data, remote: sender.to_sockaddr)
143
267
  next if conn.nil? || @sessions.key?(conn)
144
268
 
145
- @sessions[conn] = Session.attach(
146
- conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size
147
- )
148
- @remotes[conn] = sender
269
+ attach_session(conn, sender)
149
270
  end
150
271
  end
151
272
 
273
+ # Serve a new connection. One whose Session cannot be set up is logged
274
+ # and closed, and the loop goes on with the others.
275
+ def attach_session(conn, sender)
276
+ @sessions[conn] = Session.attach(
277
+ conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size,
278
+ max_connection_request_buffer: @max_connection_request_buffer, shared_buffer: @request_buffer,
279
+ request_timeout: @request_timeout, request_body_buffer_size: @request_body_buffer_size,
280
+ max_connection_request_spool: @max_connection_request_spool,
281
+ max_connection_request_spool_files: @max_connection_request_spool_files, shared_spool: @request_spool,
282
+ response_timeout: @response_timeout
283
+ )
284
+ @remotes[conn] = sender
285
+ rescue => e
286
+ log(:error, "session_error", remote: format_remote(sender), **error_fields(e))
287
+ begin
288
+ conn.close(error_code: Nghttp3::H3_INTERNAL_ERROR)
289
+ rescue QUIC::Error
290
+ nil
291
+ end
292
+ end
293
+
294
+ # Refuse with 408 the requests that ran out of time; serve_requests
295
+ # answers them.
296
+ def expire_requests
297
+ at = now
298
+ @sessions.each_value { |session| session.expire_requests(at) }
299
+ end
300
+
301
+ # Abort the responses that have waited on their clients
302
+ # response_timeout, and count them for the log.
303
+ def expire_responses
304
+ at = now
305
+ count = @sessions.each_value.sum { |session| session.expire_responses(at) }
306
+ count_timeouts(count) if count.positive?
307
+ end
308
+
309
+ # Answer the refused requests at once, then hand those received in full
310
+ # to the free workers, one at a time (#next_request).
152
311
  def serve_requests
153
312
  @sessions.each do |conn, session|
154
- session.take_requests.each do |request, status|
155
- response = status ? Response.new(status: status, headers: [], body: nil) : call_app(conn, request)
156
- begin
157
- session.respond(request, response) { |error| log_body_error(conn, request, error) }
158
- rescue Nghttp3::Error
159
- # The stream went away while the application ran.
160
- discard_body(response.body)
161
- end
313
+ session.take_refusals.each do |request, status|
314
+ count_refusal(status)
315
+ respond(conn, session, request, Response.new(status: status, headers: [], body: nil))
162
316
  end
163
317
  end
318
+ while @jobs.size < @threads
319
+ conn, request = next_request
320
+ break if request.nil?
321
+
322
+ dispatch(conn, request)
323
+ end
324
+ @adapter.take_refusals.each { |status, count| count_refusal(status, count) }
164
325
  end
165
326
 
166
- def call_app(conn, request)
167
- @adapter.call(request, remote: @remotes[conn])
168
- rescue => e
169
- log_app_error(conn, request, e)
170
- Response.new(status: 500, headers: [], body: nil)
327
+ # A method of its own, so that each block posted has a job of its own:
328
+ # a while loop's body shares its variables between rounds.
329
+ def dispatch(conn, request)
330
+ job = Job.new(conn, @sessions[conn], request, @remotes[conn], nil, false)
331
+ @jobs[job] = true
332
+ @pool.post { work(job) }
333
+ end
334
+
335
+ # The next request for the application and its connection, or nil. The
336
+ # connections take turns, one request each, and the turns go on from
337
+ # one call to the next, across turns of the loop: counted from the
338
+ # first connection every time, the oldest ones would always go first.
339
+ def next_request
340
+ conns = @sessions.keys
341
+ conns.size.times do
342
+ @next_turn = 0 if @next_turn >= conns.size
343
+ conn = conns[@next_turn]
344
+ @next_turn += 1
345
+ request = @sessions[conn].next_request
346
+ return [conn, request] if request
347
+ end
348
+ nil
349
+ end
350
+
351
+ def respond(conn, session, request, response)
352
+ session.respond(request, response) { |error| log_body_error(conn, request, error) }
353
+ rescue Nghttp3::Error
354
+ # The stream went away while the application ran.
355
+ discard_body(response.body)
171
356
  end
172
357
 
173
- def fill_bodies
358
+ # On a worker, unless stop_workers has let go of the job before a
359
+ # worker took it.
360
+ def work(job)
361
+ @jobs_lock.synchronize do
362
+ return if @workers_stopped
363
+
364
+ job.started = true
365
+ end
366
+ run_job(job)
367
+ end
368
+
369
+ # On a worker: call the application for job's request, hand the
370
+ # response to the loop, write its body, if it is read with each, and
371
+ # tell the loop the job is done, once the request's input is closed.
372
+ # The loop does the logging.
373
+ def run_job(job)
374
+ begin
375
+ response, fill = @adapter.call(job.request, remote: job.remote)
376
+ rescue Exception => e # rubocop:disable Lint/RescueException
377
+ put_result(:error, job, e)
378
+ return
379
+ end
380
+ body = response.body
381
+ # With the lock, so that stop_workers, which takes no results after
382
+ # it, either cancels the pipe and takes this response, or finds the
383
+ # workers stopped here; then the body goes nowhere.
384
+ sent = @jobs_lock.synchronize do
385
+ job.pipe = body if body.is_a?(Body::Pipe)
386
+ put_result(:response, job, response) unless @workers_stopped
387
+ !@workers_stopped
388
+ end
389
+ drop_body(body) unless sent
390
+ begin
391
+ late_error = fill&.call
392
+ rescue Exception => e # rubocop:disable Lint/RescueException
393
+ # fill fails the pipe itself; what gets out of it is the close of
394
+ # the Rack body, after the body has ended.
395
+ body.fail(e)
396
+ close_error = e
397
+ end
398
+ # What the body raised once its stream could no longer be aborted
399
+ # (after it closed the stream, say): the response has been handed
400
+ # over, so it is only logged.
401
+ put_result(:error, job, late_error) if late_error
402
+ ensure
403
+ close_input(job.request.input)
404
+ put_result(:done, job, close_error)
405
+ end
406
+
407
+ # From a worker: kind is :response (value the Response), :error (value
408
+ # what the application raised, or what its body raised after its
409
+ # response was handed over) or :done (value an exception from the close
410
+ # of the Rack body, or nil).
411
+ def put_result(kind, job, value)
412
+ @results << [kind, job, value]
413
+ wake
414
+ end
415
+
416
+ # Wake the loop, from any thread. A full pipe wakes it already.
417
+ def wake
418
+ @wake_w.write_nonblock(".", exception: false)
419
+ rescue IOError, SystemCallError
420
+ # #run has ended, and closed the pipe.
421
+ nil
422
+ end
423
+
424
+ # A body that goes nowhere, without a word: a Body::Pipe is cancelled,
425
+ # so that the worker's next write stops it, and a Body::File's own IO
426
+ # closed.
427
+ def drop_body(body)
428
+ case body
429
+ when Body::Pipe then body.cancel
430
+ when Body::File then body.io.close unless body.io.closed?
431
+ end
432
+ rescue IOError, SystemCallError
433
+ nil
434
+ end
435
+
436
+ def close_input(input)
437
+ input.close unless input.closed?
438
+ rescue IOError, SystemCallError
439
+ nil
440
+ end
441
+
442
+ def drain_wake
443
+ loop do
444
+ break unless @wake_r.read_nonblock(4096, exception: false).is_a?(String)
445
+ end
446
+ end
447
+
448
+ # What the workers put, in the order they put it.
449
+ def take_results
450
+ until @results.empty?
451
+ kind, job, value = @results.pop
452
+ case kind
453
+ when :response
454
+ deliver(job, value)
455
+ when :error
456
+ log_app_error(remote_label(job.remote), job.request, value)
457
+ # A request whose response came before finds it answered, and gets
458
+ # no 500.
459
+ deliver(job, Response.new(status: 500, headers: [], body: nil))
460
+ when :done
461
+ @jobs.delete(job)
462
+ log_body_close_error(job.request.stream_id, value) if value
463
+ job.session.finish(job.request.stream_id)
464
+ end
465
+ end
466
+ end
467
+
468
+ # Send response for job, unless its request can no longer be answered:
469
+ # the client reset it or sent STOP_SENDING, or its stream or connection
470
+ # is gone.
471
+ def deliver(job, response)
472
+ session = job.session
473
+ if @sessions[job.conn].equal?(session) && session.answerable?(job.request.stream_id)
474
+ respond(job.conn, session, job.request, response)
475
+ else
476
+ discard_body(response.body)
477
+ end
478
+ end
479
+
480
+ def count_refusal(status, count = 1)
481
+ @refusals_since ||= now
482
+ @refusals[status] += count
483
+ end
484
+
485
+ # Counted with the refusals, and logged with them.
486
+ def count_timeouts(count)
487
+ @refusals_since ||= now
488
+ @responses_timed_out += count
489
+ end
490
+
491
+ # When the refusals and timeouts counted are due to be logged; nil while
492
+ # there are none.
493
+ def refusal_log_at
494
+ @refusals_since && @refusals_since + @refusal_log_interval
495
+ end
496
+
497
+ def refusals_due?
498
+ at = refusal_log_at
499
+ !at.nil? && at <= now
500
+ end
501
+
502
+ # One line per status, in ascending order, then one for the timeouts, if
503
+ # any, with the seconds since the first of them; the count starts over.
504
+ def log_refusals
505
+ return if @refusals_since.nil?
506
+
507
+ seconds = (now - @refusals_since).round
508
+ @refusals.sort.each do |status, count|
509
+ log(:warn, "requests_refused", status: status, count: count, seconds: seconds)
510
+ end
511
+ if @responses_timed_out.positive?
512
+ log(:warn, "responses_timed_out", count: @responses_timed_out, seconds: seconds)
513
+ end
514
+ @refusals.clear
515
+ @responses_timed_out = 0
516
+ @refusals_since = nil
517
+ end
518
+
519
+ # The connections are filled in the order they came, so that while the
520
+ # total is short the older ones get its room first.
521
+ def fill_bodies(room)
174
522
  @sessions.each do |conn, session|
175
- session.fill_bodies(@send_buffer_size) { |request, error| log_app_error(conn, request, error) }
523
+ room -= session.fill_bodies(@send_buffer_size, connection_limit: @max_connection_send_buffer,
524
+ room: room) { |request, error| log_app_error(remote_of(conn), request, error) }
176
525
  end
177
526
  end
178
527
 
528
+ # What max_total_send_buffer has left for array, each and streaming
529
+ # bodies.
530
+ def send_room
531
+ @max_total_send_buffer - @sessions.each_value.sum(&:unsent_total)
532
+ end
533
+
179
534
  # Send what the Endpoint has, until the socket would block.
180
535
  def flush
181
536
  loop do
@@ -204,6 +559,9 @@ module HTTP3
204
559
  if (error = session.take_error)
205
560
  log(:error, "session_error", remote: remote_of(conn), **error_fields(error))
206
561
  end
562
+ if (error = session.take_spool_error)
563
+ log_spool_failure(conn, error)
564
+ end
207
565
  session.release_closed { |stream_id, e| log_body_close_error(stream_id, e) }
208
566
  end
209
567
  @endpoint.take_closed.each do |conn|
@@ -246,6 +604,38 @@ module HTTP3
246
604
  error: e.class, message: e.message)
247
605
  end
248
606
 
607
+ # Cancel the Body::Pipes of the jobs under way, so that the workers
608
+ # writing to them stop, let go of the jobs no worker has taken, which
609
+ # the pool drops, and wait a second for the workers, as
610
+ # close_connections waits for the CONNECTION_CLOSEs to go. A worker
611
+ # still in the application then is left to it, and counted in a warn
612
+ # line. What the workers have put and the loop not taken is dropped.
613
+ def stop_workers
614
+ waiting = @jobs_lock.synchronize do
615
+ @workers_stopped = true
616
+ @jobs.each_key { |job| job.pipe&.cancel }
617
+ @jobs.keys.reject(&:started)
618
+ end
619
+ waiting.each { |job| end_job(job) }
620
+ running = @pool.stop(1)
621
+ until @results.empty?
622
+ kind, job, value = @results.pop
623
+ case kind
624
+ when :response then drop_body(value.body)
625
+ when :done then end_job(job)
626
+ end
627
+ end
628
+ log(:warn, "workers_still_running", count: running) if running.positive?
629
+ end
630
+
631
+ # A job no longer to be heard from: its input is closed, and what its
632
+ # request holds no longer counts.
633
+ def end_job(job)
634
+ @jobs.delete(job)
635
+ close_input(job.request.input)
636
+ job.session.finish(job.request.stream_id)
637
+ end
638
+
249
639
  # Close every connection and send the CONNECTION_CLOSEs, without
250
640
  # waiting out the closing period.
251
641
  def close_connections
@@ -282,6 +672,104 @@ module HTTP3
282
672
  previous.each { |name, handler| Signal.trap(name, handler || "DEFAULT") }
283
673
  end
284
674
 
675
+ # Raise ArgumentError for the first request limit found wrong: each has
676
+ # to be an Integer in range, and the limits have to let a request at
677
+ # max_request_body and max_field_section_size through on a server with
678
+ # nothing else under way. A body over request_body_buffer_size goes to a
679
+ # temporary file, so then memory has to hold only that much of it, and
680
+ # the spool all of it.
681
+ def check_request_limits(max_request_body:, max_field_section_size:, max_connection_request_buffer:,
682
+ max_total_request_buffer:, request_body_buffer_size:, max_connection_request_spool:,
683
+ max_total_request_spool:, max_connection_request_spool_files:, max_total_request_spool_files:)
684
+ unless max_request_body.is_a?(Integer) && max_request_body >= 0
685
+ raise ArgumentError, "max_request_body must be an Integer of 0 or more (got #{max_request_body.inspect})"
686
+ end
687
+ # The range Session._attach takes, which would otherwise raise only
688
+ # on the first connection.
689
+ unless max_field_section_size.is_a?(Integer) && (1...2**62).cover?(max_field_section_size)
690
+ raise ArgumentError, "max_field_section_size must be 1..2**62-1 (got #{max_field_section_size.inspect})"
691
+ end
692
+ {max_connection_request_buffer:, max_total_request_buffer:, request_body_buffer_size:,
693
+ max_connection_request_spool:, max_total_request_spool:, max_connection_request_spool_files:,
694
+ max_total_request_spool_files:}.each do |name, value|
695
+ raise ArgumentError, "#{name} must be a positive Integer (got #{value.inspect})" unless value.is_a?(Integer) && value.positive?
696
+ end
697
+ spooled = max_request_body > request_body_buffer_size
698
+ in_memory, terms = if spooled
699
+ [request_body_buffer_size, "request_body_buffer_size + max_field_section_size"]
700
+ else
701
+ [max_request_body, "max_request_body + max_field_section_size"]
702
+ end
703
+ request = in_memory + max_field_section_size
704
+ if max_connection_request_buffer < request
705
+ raise ArgumentError, "max_connection_request_buffer (#{max_connection_request_buffer}) must be at least " \
706
+ "#{terms} (#{request})"
707
+ end
708
+ if max_total_request_buffer < max_connection_request_buffer
709
+ raise ArgumentError, "max_total_request_buffer (#{max_total_request_buffer}) must be at least " \
710
+ "max_connection_request_buffer (#{max_connection_request_buffer})"
711
+ end
712
+ if spooled && max_connection_request_spool < max_request_body
713
+ raise ArgumentError, "max_connection_request_spool (#{max_connection_request_spool}) must be at least " \
714
+ "max_request_body (#{max_request_body})"
715
+ end
716
+ if max_total_request_spool < max_connection_request_spool
717
+ raise ArgumentError, "max_total_request_spool (#{max_total_request_spool}) must be at least " \
718
+ "max_connection_request_spool (#{max_connection_request_spool})"
719
+ end
720
+ if max_total_request_spool_files < max_connection_request_spool_files
721
+ raise ArgumentError, "max_total_request_spool_files (#{max_total_request_spool_files}) must be at least " \
722
+ "max_connection_request_spool_files (#{max_connection_request_spool_files})"
723
+ end
724
+ end
725
+
726
+ # Raise ArgumentError for the first send limit found wrong: each has to
727
+ # be a positive Integer, at least the one before.
728
+ def check_send_limits(send_buffer_size, max_connection_send_buffer, max_total_send_buffer)
729
+ {send_buffer_size:, max_connection_send_buffer:, max_total_send_buffer:}.each do |name, value|
730
+ raise ArgumentError, "#{name} must be a positive Integer (got #{value.inspect})" unless value.is_a?(Integer) && value.positive?
731
+ end
732
+ if max_connection_send_buffer < send_buffer_size
733
+ raise ArgumentError, "max_connection_send_buffer (#{max_connection_send_buffer}) must be at least " \
734
+ "send_buffer_size (#{send_buffer_size})"
735
+ end
736
+ if max_total_send_buffer < max_connection_send_buffer
737
+ raise ArgumentError, "max_total_send_buffer (#{max_total_send_buffer}) must be at least " \
738
+ "max_connection_send_buffer (#{max_connection_send_buffer})"
739
+ end
740
+ end
741
+
742
+ def check_threads(threads)
743
+ raise ArgumentError, "threads must be a positive Integer (got #{threads.inspect})" unless threads.is_a?(Integer) && threads.positive?
744
+ end
745
+
746
+ def check_request_timeout(request_timeout)
747
+ return if request_timeout.nil?
748
+ return if (request_timeout.is_a?(Integer) || request_timeout.is_a?(Float)) && request_timeout.positive?
749
+
750
+ raise ArgumentError, "request_timeout must be nil or a positive number of seconds (got #{request_timeout.inspect})"
751
+ end
752
+
753
+ # response_timeout has to be less than request_timeout, so that the
754
+ # workers clients that stop reading keep are freed before the requests
755
+ # waiting for them get 503.
756
+ def check_response_timeout(response_timeout, request_timeout)
757
+ return if response_timeout.nil?
758
+
759
+ seconds = (response_timeout.is_a?(Integer) || response_timeout.is_a?(Float)) && response_timeout.positive?
760
+ raise ArgumentError, "response_timeout must be nil or a positive number of seconds (got #{response_timeout.inspect})" unless seconds
761
+ return if request_timeout.nil? || response_timeout < request_timeout
762
+
763
+ raise ArgumentError, "response_timeout (#{response_timeout}) must be less than request_timeout (#{request_timeout})"
764
+ end
765
+
766
+ def check_refusal_log_interval(refusal_log_interval)
767
+ return if (refusal_log_interval.is_a?(Integer) || refusal_log_interval.is_a?(Float)) && refusal_log_interval.positive?
768
+
769
+ raise ArgumentError,
770
+ "refusal_log_interval must be a positive number of seconds (got #{refusal_log_interval.inspect})"
771
+ end
772
+
285
773
  def parse_bind(bind)
286
774
  match = bind.match(/\A\[(?<host>[^\]]+)\]:(?<port>\d+)\z/) || bind.match(/\A(?<host>[^:\[\]]+):(?<port>\d+)\z/)
287
775
  raise ArgumentError, "bind must be \"host:port\" or \"[ipv6]:port\" (got #{bind.inspect})" unless match
@@ -293,22 +781,37 @@ module HTTP3
293
781
  address
294
782
  end
295
783
 
784
+ # A body that will not be sent: a Body::Pipe is cancelled, so that its
785
+ # worker stops writing, and a Body::File's own IO closed. The Rack body
786
+ # is the worker's to close.
296
787
  def discard_body(body)
297
- return if body.nil?
298
-
299
- body.io.close if body.is_a?(Body::File) && !body.io.closed?
300
- body.rack_body.close if body.rack_body.respond_to?(:close)
788
+ case body
789
+ when Body::Pipe then body.cancel
790
+ when Body::File then body.io.close unless body.io.closed?
791
+ end
301
792
  rescue => e
302
793
  log(:error, "body_close_error", **error_fields(e))
303
794
  end
304
795
 
305
- def log_app_error(conn, request, error)
796
+ def log_app_error(remote, request, error)
306
797
  pseudo = request.headers.select { |name, _| name.start_with?(":") }.to_h
307
798
  path = pseudo.fetch(":path", "")[/\A[^?]*/]
308
- log(:error, "app_error", remote: remote_of(conn), stream: request.stream_id, method: pseudo[":method"],
799
+ log(:error, "app_error", remote: remote, stream: request.stream_id, method: pseudo[":method"],
309
800
  path: path, **error_fields(error))
310
801
  end
311
802
 
803
+ # A temporary file for a request body could not be made or written
804
+ # (the request got 503). While the disk is full every large request
805
+ # fails, so after one line, those within refusal_log_interval seconds
806
+ # are not logged.
807
+ def log_spool_failure(conn, error)
808
+ at = now
809
+ return if @spool_failure_logged_at && at < @spool_failure_logged_at + @refusal_log_interval
810
+
811
+ @spool_failure_logged_at = at
812
+ log(:error, "request_spool_failed", remote: remote_of(conn), error: error.class, message: error.message)
813
+ end
814
+
312
815
  def log_body_error(conn, request, error)
313
816
  log(:error, "body_error", remote: remote_of(conn), stream: request.stream_id, error: error.class,
314
817
  message: error.message)
@@ -337,7 +840,14 @@ module HTTP3
337
840
  end
338
841
 
339
842
  def remote_of(conn)
340
- address = @remotes[conn] or return "-"
843
+ remote_label(@remotes[conn])
844
+ end
845
+
846
+ def remote_label(address)
847
+ address ? format_remote(address) : "-"
848
+ end
849
+
850
+ def format_remote(address)
341
851
  host = address.ipv6? ? "[#{address.ip_address}]" : address.ip_address
342
852
  "#{host}:#{address.ip_port}"
343
853
  end