http3 0.0.4 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/LICENSE-DEPENDENCIES.txt +2 -2
- data/README.md +76 -17
- data/ext/http3/extconf.rb +8 -5
- data/ext/http3/nghttp3.c +1 -1
- data/ext/http3/nghttp3.h +3 -1
- data/ext/http3/nghttp3_callbacks.c +8 -10
- data/ext/http3/nghttp3_connection.c +1 -1
- data/ext/http3/nghttp3_qpack.c +2 -2
- data/ext/http3/session.c +234 -9
- data/lib/http3/body_pipe.rb +160 -0
- data/lib/http3/body_stream.rb +120 -0
- data/lib/http3/rack_adapter.rb +114 -47
- data/lib/http3/request.rb +4 -2
- data/lib/http3/server.rb +402 -62
- data/lib/http3/session.rb +458 -89
- data/lib/http3/version.rb +1 -1
- data/lib/http3/worker_pool.rb +71 -0
- data/lib/http3.rb +3 -0
- data/sig/http3/body_pipe.rbs +45 -0
- data/sig/http3/body_stream.rbs +41 -0
- data/sig/http3/rack_adapter.rbs +21 -11
- data/sig/http3/request.rbs +5 -3
- data/sig/http3/response.rbs +2 -2
- data/sig/http3/server.rbs +8 -1
- data/sig/http3/session.rbs +104 -25
- data/sig/http3/worker_pool.rbs +19 -0
- metadata +9 -6
- data/.clang-format +0 -1
- data/.vscode/extensions.json +0 -6
- data/.vscode/settings.json +0 -10
data/lib/http3/server.rb
CHANGED
|
@@ -20,13 +20,28 @@ module HTTP3
|
|
|
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
22
|
# strings and header values never appear in the log. Those answered
|
|
23
|
-
# without the application (400, 408, 413, 431, 501, 503)
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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.
|
|
27
35
|
class Server
|
|
28
36
|
ALPN = ["h3"].freeze
|
|
29
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
|
+
|
|
30
45
|
# What the signal handlers write to the self-pipe.
|
|
31
46
|
SIGNALS = {"TERM" => "T", "INT" => "I", "HUP" => "H"}.freeze
|
|
32
47
|
|
|
@@ -46,30 +61,54 @@ module HTTP3
|
|
|
46
61
|
# certificate_path is a PEM chain, leaf first; private_key_path a PEM key
|
|
47
62
|
# without passphrase. qlog_dir writes one qlog file per connection.
|
|
48
63
|
# max_request_body and max_field_section_size (bytes) bound a request's
|
|
49
|
-
# body (413 beyond) and header section (431 beyond).
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
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
|
|
55
77
|
# bounds the bytes of a response body queued but unsent per stream,
|
|
56
|
-
# max_connection_send_buffer those of the array
|
|
57
|
-
# connection, and max_total_send_buffer those of every
|
|
58
|
-
# body waits for room. Each has to be at least the one before. A request
|
|
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
|
|
59
81
|
# not received in full within request_timeout seconds of the start of
|
|
60
|
-
# its headers gets 408
|
|
61
|
-
#
|
|
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.
|
|
62
91
|
def initialize(app:, bind:, certificate_path:, private_key_path:, transport_params: nil, settings: nil,
|
|
63
|
-
qlog_dir: nil, max_connections: 1024, max_request_body: 1_048_576, max_field_section_size: 65_536,
|
|
92
|
+
qlog_dir: nil, max_connections: 1024, threads: 3, max_request_body: 1_048_576, max_field_section_size: 65_536,
|
|
64
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,
|
|
65
97
|
send_buffer_size: 65_536, max_connection_send_buffer: 1_048_576, max_total_send_buffer: 67_108_864,
|
|
66
|
-
request_timeout: 30, refusal_log_interval: 60, shutdown_timeout: 30,
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
69
105
|
check_send_limits(send_buffer_size, max_connection_send_buffer, max_total_send_buffer)
|
|
106
|
+
check_threads(threads)
|
|
70
107
|
check_request_timeout(request_timeout)
|
|
108
|
+
check_response_timeout(response_timeout, request_timeout)
|
|
71
109
|
check_refusal_log_interval(refusal_log_interval)
|
|
72
|
-
@
|
|
110
|
+
@app = app
|
|
111
|
+
@threads = threads
|
|
73
112
|
@bind_address = parse_bind(bind)
|
|
74
113
|
@certificate_path = certificate_path
|
|
75
114
|
@private_key_path = private_key_path
|
|
@@ -84,10 +123,16 @@ module HTTP3
|
|
|
84
123
|
@max_field_section_size = max_field_section_size
|
|
85
124
|
@max_connection_request_buffer = max_connection_request_buffer
|
|
86
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
|
|
87
131
|
@send_buffer_size = send_buffer_size
|
|
88
132
|
@max_connection_send_buffer = max_connection_send_buffer
|
|
89
133
|
@max_total_send_buffer = max_total_send_buffer
|
|
90
134
|
@request_timeout = request_timeout
|
|
135
|
+
@response_timeout = response_timeout
|
|
91
136
|
@refusal_log_interval = refusal_log_interval
|
|
92
137
|
@shutdown_timeout = shutdown_timeout
|
|
93
138
|
@logger = logger
|
|
@@ -96,6 +141,17 @@ module HTTP3
|
|
|
96
141
|
# Serve until told to stop (see the class comment). Returns nil.
|
|
97
142
|
def run
|
|
98
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))
|
|
99
155
|
@sock = UDPSocket.new(@bind_address.afamily)
|
|
100
156
|
@sock.bind(@bind_address.ip_address, @bind_address.ip_port)
|
|
101
157
|
@local_address = @sock.local_address
|
|
@@ -105,24 +161,35 @@ module HTTP3
|
|
|
105
161
|
)
|
|
106
162
|
@sessions = {}
|
|
107
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
|
|
108
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
|
|
109
171
|
@blocked_send = nil
|
|
110
172
|
@phase = :serving
|
|
111
173
|
@drain_deadline = nil
|
|
112
174
|
# {status => count} of the requests answered without the
|
|
113
175
|
# application, since @refusals_since.
|
|
114
176
|
@refusals = Hash.new(0)
|
|
177
|
+
# The responses aborted after response_timeout, since @refusals_since.
|
|
178
|
+
@responses_timed_out = 0
|
|
115
179
|
@refusals_since = nil
|
|
116
180
|
old_traps = install_traps(sig_w)
|
|
181
|
+
@pool = WorkerPool.new(@threads).start
|
|
117
182
|
|
|
118
183
|
turn(sig_r) until finished?
|
|
119
184
|
close_connections
|
|
185
|
+
stop_workers
|
|
120
186
|
log_refusals
|
|
121
187
|
nil
|
|
122
188
|
ensure
|
|
123
189
|
restore_traps(old_traps) if old_traps
|
|
190
|
+
stop_workers if @pool && !@workers_stopped
|
|
124
191
|
@sessions&.each_value { |session| release_session(session) }
|
|
125
|
-
[@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? }
|
|
126
193
|
end
|
|
127
194
|
|
|
128
195
|
private
|
|
@@ -134,11 +201,16 @@ module HTTP3
|
|
|
134
201
|
def turn(sig_r)
|
|
135
202
|
room = send_room
|
|
136
203
|
writers = @blocked_send ? [@sock] : nil
|
|
137
|
-
readable, writable, = IO.select([@sock, sig_r], writers, nil, select_timeout(room))
|
|
204
|
+
readable, writable, = IO.select([@sock, sig_r, @wake_r], writers, nil, select_timeout(room))
|
|
138
205
|
handle_signals(sig_r) if readable&.include?(sig_r)
|
|
139
206
|
flush if writable&.include?(@sock)
|
|
140
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
|
|
141
212
|
expire_requests
|
|
213
|
+
expire_responses
|
|
142
214
|
serve_requests
|
|
143
215
|
log_refusals if refusals_due?
|
|
144
216
|
fill_bodies(room)
|
|
@@ -150,14 +222,17 @@ module HTTP3
|
|
|
150
222
|
def finished?
|
|
151
223
|
case @phase
|
|
152
224
|
when :serving then false
|
|
153
|
-
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?))
|
|
154
226
|
else true
|
|
155
227
|
end
|
|
156
228
|
end
|
|
157
229
|
|
|
158
|
-
# Until the next timer, the next request
|
|
159
|
-
#
|
|
160
|
-
#
|
|
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.
|
|
161
236
|
def select_timeout(room)
|
|
162
237
|
wants_fill = @sessions.each_value.any? { |session|
|
|
163
238
|
session.wants_fill?(@send_buffer_size, connection_limit: @max_connection_send_buffer, room: room)
|
|
@@ -169,6 +244,8 @@ module HTTP3
|
|
|
169
244
|
timeouts << [(expiry - now_ns) / 1e9, 0.0].max if expiry
|
|
170
245
|
deadline = @sessions.each_value.filter_map(&:next_request_deadline).min
|
|
171
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
|
|
172
249
|
timeouts << [refusal_log_at - now, 0.0].max if refusal_log_at
|
|
173
250
|
timeouts << [@drain_deadline - now, 0.0].max if @drain_deadline
|
|
174
251
|
timeouts.min
|
|
@@ -199,7 +276,10 @@ module HTTP3
|
|
|
199
276
|
@sessions[conn] = Session.attach(
|
|
200
277
|
conn, max_request_body: @max_request_body, max_field_section_size: @max_field_section_size,
|
|
201
278
|
max_connection_request_buffer: @max_connection_request_buffer, shared_buffer: @request_buffer,
|
|
202
|
-
request_timeout: @request_timeout
|
|
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
|
|
203
283
|
)
|
|
204
284
|
@remotes[conn] = sender
|
|
205
285
|
rescue => e
|
|
@@ -218,27 +298,183 @@ module HTTP3
|
|
|
218
298
|
@sessions.each_value { |session| session.expire_requests(at) }
|
|
219
299
|
end
|
|
220
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).
|
|
221
311
|
def serve_requests
|
|
222
312
|
@sessions.each do |conn, session|
|
|
223
|
-
session.
|
|
224
|
-
count_refusal(status)
|
|
225
|
-
|
|
226
|
-
begin
|
|
227
|
-
session.respond(request, response) { |error| log_body_error(conn, request, error) }
|
|
228
|
-
rescue Nghttp3::Error
|
|
229
|
-
# The stream went away while the application ran.
|
|
230
|
-
discard_body(response.body)
|
|
231
|
-
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))
|
|
232
316
|
end
|
|
233
317
|
end
|
|
318
|
+
while @jobs.size < @threads
|
|
319
|
+
conn, request = next_request
|
|
320
|
+
break if request.nil?
|
|
321
|
+
|
|
322
|
+
dispatch(conn, request)
|
|
323
|
+
end
|
|
234
324
|
@adapter.take_refusals.each { |status, count| count_refusal(status, count) }
|
|
235
325
|
end
|
|
236
326
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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)
|
|
356
|
+
end
|
|
357
|
+
|
|
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
|
|
242
478
|
end
|
|
243
479
|
|
|
244
480
|
def count_refusal(status, count = 1)
|
|
@@ -246,8 +482,14 @@ module HTTP3
|
|
|
246
482
|
@refusals[status] += count
|
|
247
483
|
end
|
|
248
484
|
|
|
249
|
-
#
|
|
250
|
-
|
|
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.
|
|
251
493
|
def refusal_log_at
|
|
252
494
|
@refusals_since && @refusals_since + @refusal_log_interval
|
|
253
495
|
end
|
|
@@ -257,8 +499,8 @@ module HTTP3
|
|
|
257
499
|
!at.nil? && at <= now
|
|
258
500
|
end
|
|
259
501
|
|
|
260
|
-
# One line per status, in ascending order,
|
|
261
|
-
# first of them; the count starts over.
|
|
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.
|
|
262
504
|
def log_refusals
|
|
263
505
|
return if @refusals_since.nil?
|
|
264
506
|
|
|
@@ -266,7 +508,11 @@ module HTTP3
|
|
|
266
508
|
@refusals.sort.each do |status, count|
|
|
267
509
|
log(:warn, "requests_refused", status: status, count: count, seconds: seconds)
|
|
268
510
|
end
|
|
511
|
+
if @responses_timed_out.positive?
|
|
512
|
+
log(:warn, "responses_timed_out", count: @responses_timed_out, seconds: seconds)
|
|
513
|
+
end
|
|
269
514
|
@refusals.clear
|
|
515
|
+
@responses_timed_out = 0
|
|
270
516
|
@refusals_since = nil
|
|
271
517
|
end
|
|
272
518
|
|
|
@@ -275,11 +521,12 @@ module HTTP3
|
|
|
275
521
|
def fill_bodies(room)
|
|
276
522
|
@sessions.each do |conn, session|
|
|
277
523
|
room -= session.fill_bodies(@send_buffer_size, connection_limit: @max_connection_send_buffer,
|
|
278
|
-
room: room) { |request, error| log_app_error(conn, request, error) }
|
|
524
|
+
room: room) { |request, error| log_app_error(remote_of(conn), request, error) }
|
|
279
525
|
end
|
|
280
526
|
end
|
|
281
527
|
|
|
282
|
-
# What max_total_send_buffer has left for array and
|
|
528
|
+
# What max_total_send_buffer has left for array, each and streaming
|
|
529
|
+
# bodies.
|
|
283
530
|
def send_room
|
|
284
531
|
@max_total_send_buffer - @sessions.each_value.sum(&:unsent_total)
|
|
285
532
|
end
|
|
@@ -312,6 +559,9 @@ module HTTP3
|
|
|
312
559
|
if (error = session.take_error)
|
|
313
560
|
log(:error, "session_error", remote: remote_of(conn), **error_fields(error))
|
|
314
561
|
end
|
|
562
|
+
if (error = session.take_spool_error)
|
|
563
|
+
log_spool_failure(conn, error)
|
|
564
|
+
end
|
|
315
565
|
session.release_closed { |stream_id, e| log_body_close_error(stream_id, e) }
|
|
316
566
|
end
|
|
317
567
|
@endpoint.take_closed.each do |conn|
|
|
@@ -354,6 +604,38 @@ module HTTP3
|
|
|
354
604
|
error: e.class, message: e.message)
|
|
355
605
|
end
|
|
356
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
|
+
|
|
357
639
|
# Close every connection and send the CONNECTION_CLOSEs, without
|
|
358
640
|
# waiting out the closing period.
|
|
359
641
|
def close_connections
|
|
@@ -391,11 +673,14 @@ module HTTP3
|
|
|
391
673
|
end
|
|
392
674
|
|
|
393
675
|
# Raise ArgumentError for the first request limit found wrong: each has
|
|
394
|
-
# to be an Integer in range, and the
|
|
676
|
+
# to be an Integer in range, and the limits have to let a request at
|
|
395
677
|
# max_request_body and max_field_section_size through on a server with
|
|
396
|
-
# nothing else under way.
|
|
397
|
-
|
|
398
|
-
|
|
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:)
|
|
399
684
|
unless max_request_body.is_a?(Integer) && max_request_body >= 0
|
|
400
685
|
raise ArgumentError, "max_request_body must be an Integer of 0 or more (got #{max_request_body.inspect})"
|
|
401
686
|
end
|
|
@@ -404,18 +689,38 @@ module HTTP3
|
|
|
404
689
|
unless max_field_section_size.is_a?(Integer) && (1...2**62).cover?(max_field_section_size)
|
|
405
690
|
raise ArgumentError, "max_field_section_size must be 1..2**62-1 (got #{max_field_section_size.inspect})"
|
|
406
691
|
end
|
|
407
|
-
{max_connection_request_buffer:, max_total_request_buffer
|
|
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|
|
|
408
695
|
raise ArgumentError, "#{name} must be a positive Integer (got #{value.inspect})" unless value.is_a?(Integer) && value.positive?
|
|
409
696
|
end
|
|
410
|
-
|
|
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
|
|
411
704
|
if max_connection_request_buffer < request
|
|
412
705
|
raise ArgumentError, "max_connection_request_buffer (#{max_connection_request_buffer}) must be at least " \
|
|
413
|
-
"
|
|
706
|
+
"#{terms} (#{request})"
|
|
414
707
|
end
|
|
415
708
|
if max_total_request_buffer < max_connection_request_buffer
|
|
416
709
|
raise ArgumentError, "max_total_request_buffer (#{max_total_request_buffer}) must be at least " \
|
|
417
710
|
"max_connection_request_buffer (#{max_connection_request_buffer})"
|
|
418
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
|
|
419
724
|
end
|
|
420
725
|
|
|
421
726
|
# Raise ArgumentError for the first send limit found wrong: each has to
|
|
@@ -434,6 +739,10 @@ module HTTP3
|
|
|
434
739
|
end
|
|
435
740
|
end
|
|
436
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
|
+
|
|
437
746
|
def check_request_timeout(request_timeout)
|
|
438
747
|
return if request_timeout.nil?
|
|
439
748
|
return if (request_timeout.is_a?(Integer) || request_timeout.is_a?(Float)) && request_timeout.positive?
|
|
@@ -441,6 +750,19 @@ module HTTP3
|
|
|
441
750
|
raise ArgumentError, "request_timeout must be nil or a positive number of seconds (got #{request_timeout.inspect})"
|
|
442
751
|
end
|
|
443
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
|
+
|
|
444
766
|
def check_refusal_log_interval(refusal_log_interval)
|
|
445
767
|
return if (refusal_log_interval.is_a?(Integer) || refusal_log_interval.is_a?(Float)) && refusal_log_interval.positive?
|
|
446
768
|
|
|
@@ -459,22 +781,37 @@ module HTTP3
|
|
|
459
781
|
address
|
|
460
782
|
end
|
|
461
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.
|
|
462
787
|
def discard_body(body)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
body.io.close
|
|
466
|
-
|
|
788
|
+
case body
|
|
789
|
+
when Body::Pipe then body.cancel
|
|
790
|
+
when Body::File then body.io.close unless body.io.closed?
|
|
791
|
+
end
|
|
467
792
|
rescue => e
|
|
468
793
|
log(:error, "body_close_error", **error_fields(e))
|
|
469
794
|
end
|
|
470
795
|
|
|
471
|
-
def log_app_error(
|
|
796
|
+
def log_app_error(remote, request, error)
|
|
472
797
|
pseudo = request.headers.select { |name, _| name.start_with?(":") }.to_h
|
|
473
798
|
path = pseudo.fetch(":path", "")[/\A[^?]*/]
|
|
474
|
-
log(:error, "app_error", remote:
|
|
799
|
+
log(:error, "app_error", remote: remote, stream: request.stream_id, method: pseudo[":method"],
|
|
475
800
|
path: path, **error_fields(error))
|
|
476
801
|
end
|
|
477
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
|
+
|
|
478
815
|
def log_body_error(conn, request, error)
|
|
479
816
|
log(:error, "body_error", remote: remote_of(conn), stream: request.stream_id, error: error.class,
|
|
480
817
|
message: error.message)
|
|
@@ -503,8 +840,11 @@ module HTTP3
|
|
|
503
840
|
end
|
|
504
841
|
|
|
505
842
|
def remote_of(conn)
|
|
506
|
-
|
|
507
|
-
|
|
843
|
+
remote_label(@remotes[conn])
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def remote_label(address)
|
|
847
|
+
address ? format_remote(address) : "-"
|
|
508
848
|
end
|
|
509
849
|
|
|
510
850
|
def format_remote(address)
|