connectrpc-async 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +542 -0
- data/lib/connectrpc/async/application.rb +173 -0
- data/lib/connectrpc/async/completion_join.rb +249 -0
- data/lib/connectrpc/async/connection.rb +711 -0
- data/lib/connectrpc/async/connection_manager.rb +358 -0
- data/lib/connectrpc/async/connection_policy.rb +60 -0
- data/lib/connectrpc/async/host.rb +171 -0
- data/lib/connectrpc/async/http1_connection.rb +144 -0
- data/lib/connectrpc/async/http1_exchange.rb +27 -0
- data/lib/connectrpc/async/http1_stream.rb +76 -0
- data/lib/connectrpc/async/http1_transport.rb +642 -0
- data/lib/connectrpc/async/ingress.rb +100 -0
- data/lib/connectrpc/async/internal.rb +270 -0
- data/lib/connectrpc/async/server_session.rb +457 -0
- data/lib/connectrpc/async/stream_exchange.rb +324 -0
- data/lib/connectrpc/async/tls.rb +124 -0
- data/lib/connectrpc/async/tls_configuration.rb +309 -0
- data/lib/connectrpc/async/transport.rb +488 -0
- data/lib/connectrpc/async/version.rb +7 -0
- data/lib/connectrpc/async.rb +39 -0
- data/sig/connectrpc-async.rbs +165 -0
- data/sig/manifest.yaml +4 -0
- metadata +202 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module ConnectRPC
|
|
6
|
+
module Async
|
|
7
|
+
module Internal
|
|
8
|
+
def self.settings(max_requests)
|
|
9
|
+
{
|
|
10
|
+
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
|
|
11
|
+
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => max_requests,
|
|
12
|
+
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 16_384,
|
|
13
|
+
::Protocol::HTTP2::Settings::INITIAL_WINDOW_SIZE => 65_536
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised before a writer enters the guard of a connection that can no
|
|
18
|
+
# longer start streams. Nothing has been written for that writer.
|
|
19
|
+
class UnusableConnection < IOError; end
|
|
20
|
+
|
|
21
|
+
# Version-coupled wrappers around the selected stack, not a frame codec.
|
|
22
|
+
module Connection
|
|
23
|
+
attr_accessor :resource, :defer_close
|
|
24
|
+
attr_reader :writers, :reader_task
|
|
25
|
+
|
|
26
|
+
def initialize(...)
|
|
27
|
+
super
|
|
28
|
+
@writers = {}
|
|
29
|
+
@writer = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A dispatcher has allocated and written nothing while it waits, so it
|
|
33
|
+
# is not a retirement writer until it holds the guard. It then rechecks
|
|
34
|
+
# before registering, without yielding, and returns to acquisition.
|
|
35
|
+
def synchronize(dispatch: false)
|
|
36
|
+
task = ::Async::Task.current
|
|
37
|
+
return yield if @writer.equal?(task)
|
|
38
|
+
|
|
39
|
+
raise UnusableConnection, "HTTP/2 connection closed" if @resource&.finished?
|
|
40
|
+
|
|
41
|
+
@writers[task] = true unless dispatch
|
|
42
|
+
begin
|
|
43
|
+
@write_frame_guard.acquire do
|
|
44
|
+
if dispatch
|
|
45
|
+
raise UnusableConnection, "HTTP/2 connection closed" if @resource&.finished?
|
|
46
|
+
|
|
47
|
+
@writers[task] = true
|
|
48
|
+
end
|
|
49
|
+
@writer = task
|
|
50
|
+
begin
|
|
51
|
+
yield
|
|
52
|
+
ensure
|
|
53
|
+
@writer = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
ensure
|
|
57
|
+
@writers.delete(task)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Acquire while cancellable. Once a frame starts, encode/flush/state
|
|
62
|
+
# must finish together; a second cancellation is reserved for raw abort.
|
|
63
|
+
def write_frame(frame)
|
|
64
|
+
synchronize { ::Async::Task.current.defer_cancel { super } }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def write_frames(&)
|
|
68
|
+
synchronize { ::Async::Task.current.defer_cancel { super(&) } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Mirror async-http 0.105.0 and Async 2.46's eager Task#async, publishing
|
|
72
|
+
# the actual transient reader before EOF/error can reenter retirement.
|
|
73
|
+
def read_in_background(parent: ::Async::Task.current)
|
|
74
|
+
raise "Connection is closed!" if closed?
|
|
75
|
+
raise ::Async::Task::FinishedError if parent.finished?
|
|
76
|
+
|
|
77
|
+
reader = @reader_task = ::Async::Task.new(parent, transient: true) do |task|
|
|
78
|
+
@reader = task
|
|
79
|
+
task.annotate("#{version} reading data for #{self.class}.")
|
|
80
|
+
begin
|
|
81
|
+
until closed?
|
|
82
|
+
consume_window
|
|
83
|
+
read_frame
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
# Upstream forwards the original reader failure through close.
|
|
87
|
+
ensure
|
|
88
|
+
if @reader
|
|
89
|
+
@reader = nil
|
|
90
|
+
close(e)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
reader.run
|
|
95
|
+
reader
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def consume_local_window(frame)
|
|
99
|
+
if frame.length > @local_window.available
|
|
100
|
+
raise ::Protocol::HTTP2::FlowControlError, "DATA exceeds connection receive window"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def delete(id)
|
|
107
|
+
super
|
|
108
|
+
ensure
|
|
109
|
+
@resource&.changed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def close(error = nil)
|
|
113
|
+
@requests&.close
|
|
114
|
+
if @defer_close
|
|
115
|
+
@resource&.changed
|
|
116
|
+
elsif @resource
|
|
117
|
+
@resource.abort(error)
|
|
118
|
+
else
|
|
119
|
+
super
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def finish_close(error = nil)
|
|
124
|
+
@requests&.close
|
|
125
|
+
# Skip our override while retaining upstream queue/reader/framer cleanup.
|
|
126
|
+
super_method = method(:close).super_method
|
|
127
|
+
super_method.call(error)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class GRPCHeaderError < ::Protocol::HTTP2::HeaderError
|
|
132
|
+
def initialize(message, code)
|
|
133
|
+
super(message)
|
|
134
|
+
@code = code
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Publish ownership before the pinned output's eager first body read.
|
|
139
|
+
class Output < ::Async::HTTP::Protocol::HTTP2::Output
|
|
140
|
+
def start(parent: ::Async::Task.current)
|
|
141
|
+
raise "Task already started!" if @task
|
|
142
|
+
|
|
143
|
+
work = method(@body.stream? ? :stream : :passthrough)
|
|
144
|
+
task = @task = ::Async::Task.new(parent, &work)
|
|
145
|
+
yield task
|
|
146
|
+
task.run
|
|
147
|
+
task
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
module Stream # rubocop:disable Metrics/ModuleLength
|
|
152
|
+
attr_reader :grpc_initial_headers, :grpc_initial_end_stream, :grpc_trailers, :grpc_error, :grpc_reset,
|
|
153
|
+
:declared_length
|
|
154
|
+
|
|
155
|
+
def grpc_mark! = @grpc = true
|
|
156
|
+
def grpc? = !!@grpc
|
|
157
|
+
|
|
158
|
+
def grpc_fields(headers)
|
|
159
|
+
size = headers.sum { |key, value| key.bytesize + value.bytesize + 32 }
|
|
160
|
+
if size > GRPCProtocol::FIELD_LIMIT
|
|
161
|
+
@grpc_error = Error.new(:resource_exhausted, "gRPC field block exceeds byte limit")
|
|
162
|
+
raise GRPCHeaderError.new("gRPC field block exceeds byte limit", ::Protocol::HTTP2::Error::ENHANCE_YOUR_CALM)
|
|
163
|
+
end
|
|
164
|
+
values = {}
|
|
165
|
+
headers.each { |key, value| (values[key] ||= []) << value unless key.start_with?(":") }
|
|
166
|
+
GRPCProtocol.snapshot(values)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def receive_initial_headers(headers, end_stream)
|
|
170
|
+
@grpc_web ||= headers.any? do |key, value|
|
|
171
|
+
key.casecmp?("content-type") && value.downcase.start_with?("application/grpc-web")
|
|
172
|
+
end
|
|
173
|
+
@grpc ||= !@grpc_web && headers.any? do |key, value|
|
|
174
|
+
key.casecmp?("content-type") && value.downcase.start_with?("application/grpc")
|
|
175
|
+
end
|
|
176
|
+
grpc_fields(headers) if @grpc_web
|
|
177
|
+
if @grpc
|
|
178
|
+
fields = grpc_fields(headers)
|
|
179
|
+
status = headers.find { |key, _value| key == ":status" }&.last.to_i
|
|
180
|
+
informational = status.between?(100, 199)
|
|
181
|
+
if informational && end_stream
|
|
182
|
+
@grpc_error = Error.new(:internal, "Informational gRPC HEADERS ended the stream")
|
|
183
|
+
raise GRPCHeaderError.new("Informational END_STREAM", ::Protocol::HTTP2::Error::PROTOCOL_ERROR)
|
|
184
|
+
end
|
|
185
|
+
if @grpc_initial_headers
|
|
186
|
+
@grpc_error = Error.new(:internal, "Nonfinal second gRPC HEADERS")
|
|
187
|
+
raise GRPCHeaderError.new("Nonfinal second gRPC HEADERS", ::Protocol::HTTP2::Error::PROTOCOL_ERROR)
|
|
188
|
+
end
|
|
189
|
+
unless informational
|
|
190
|
+
@grpc_initial_headers = fields
|
|
191
|
+
@grpc_initial_end_stream = end_stream
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
super
|
|
195
|
+
ensure
|
|
196
|
+
@declared_length = @length
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def receive_trailing_headers(headers, end_stream)
|
|
200
|
+
grpc_fields(headers) if @grpc_web
|
|
201
|
+
if @grpc
|
|
202
|
+
@grpc_trailers = grpc_fields(headers)
|
|
203
|
+
unless end_stream
|
|
204
|
+
@grpc_error = Error.new(:internal, "Nonfinal gRPC trailing HEADERS")
|
|
205
|
+
raise GRPCHeaderError.new("Nonfinal gRPC trailing HEADERS", ::Protocol::HTTP2::Error::PROTOCOL_ERROR)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
@headers.trailer!
|
|
209
|
+
super
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def prepare_input(length)
|
|
213
|
+
raise ArgumentError, "Input body already prepared!" if @input
|
|
214
|
+
|
|
215
|
+
@reclaimed_input = 0
|
|
216
|
+
@input = Ingress.new(self, length)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def receive_data(frame)
|
|
220
|
+
if %i[open half_closed_local].include?(@state) && frame.length > @local_window.available
|
|
221
|
+
@input&.retire(::Protocol::HTTP2::StreamError.for(::Protocol::HTTP2::Error::FLOW_CONTROL_ERROR))
|
|
222
|
+
send_reset_stream(::Protocol::HTTP2::Error::FLOW_CONTROL_ERROR)
|
|
223
|
+
return
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
super
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def receive_reset_stream(frame)
|
|
230
|
+
if @grpc
|
|
231
|
+
@grpc_reset = frame.unpack
|
|
232
|
+
if @grpc_reset == ::Protocol::HTTP2::Error::NO_ERROR
|
|
233
|
+
fields = @grpc_initial_end_stream ? @grpc_initial_headers : @grpc_trailers
|
|
234
|
+
statuses = fields && fields["grpc-status"]
|
|
235
|
+
unless statuses && statuses.size == 1 && !GRPCProtocol.status_number(statuses.first).nil?
|
|
236
|
+
@grpc_error ||= Error.new(:internal, "HTTP/2 NO_ERROR reset without gRPC status")
|
|
237
|
+
end
|
|
238
|
+
else
|
|
239
|
+
code = { 3 => :resource_exhausted, 7 => :unavailable, 8 => :canceled,
|
|
240
|
+
11 => :resource_exhausted, 12 => :permission_denied }.fetch(@grpc_reset, :internal)
|
|
241
|
+
@grpc_error ||= Error.new(code, "HTTP/2 stream reset")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
unless @state == :idle || frame.unpack == ::Protocol::HTTP2::Error::NO_ERROR
|
|
245
|
+
@input&.retire(nil, discard: true)
|
|
246
|
+
end
|
|
247
|
+
super
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def process_data(frame)
|
|
251
|
+
@grpc_data_end_stream = true if @grpc && frame.end_stream?
|
|
252
|
+
data = frame.unpack
|
|
253
|
+
if (input = @input)
|
|
254
|
+
input.write(data) unless data.empty?
|
|
255
|
+
input.close_write if frame.end_stream?
|
|
256
|
+
discarded = frame.length - data.bytesize
|
|
257
|
+
else
|
|
258
|
+
discarded = frame.length
|
|
259
|
+
end
|
|
260
|
+
reclaim_input(discarded)
|
|
261
|
+
data
|
|
262
|
+
rescue ::Protocol::HTTP2::ProtocolError
|
|
263
|
+
raise
|
|
264
|
+
rescue StandardError
|
|
265
|
+
send_reset_stream(::Protocol::HTTP2::Error::INTERNAL_ERROR)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def reclaim_input(amount)
|
|
269
|
+
return if closed? || @state == :half_closed_remote || @connection.closed?
|
|
270
|
+
|
|
271
|
+
@reclaimed_input = (@reclaimed_input || 0) + amount
|
|
272
|
+
request_window_update
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def request_window_update
|
|
276
|
+
return unless @reclaimed_input&.positive? && @local_window.limited?
|
|
277
|
+
|
|
278
|
+
@connection.synchronize do
|
|
279
|
+
next if closed? || @state == :half_closed_remote || @connection.closed?
|
|
280
|
+
next unless @reclaimed_input.positive? && @local_window.limited?
|
|
281
|
+
|
|
282
|
+
::Async::Task.current.defer_cancel do
|
|
283
|
+
amount = @reclaimed_input
|
|
284
|
+
@reclaimed_input = 0
|
|
285
|
+
# Reserve credit before flush can yield to the reader. A peer
|
|
286
|
+
# may spend the update as soon as it reaches the socket.
|
|
287
|
+
@local_window.expand(amount)
|
|
288
|
+
frame = ::Protocol::HTTP2::WindowUpdateFrame.new(@id)
|
|
289
|
+
frame.pack(amount)
|
|
290
|
+
write_frame(frame)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def closed(error)
|
|
296
|
+
@reclaimed_input = 0
|
|
297
|
+
@input&.retire(error)
|
|
298
|
+
super
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def finish_output(error = nil)
|
|
302
|
+
if (@grpc || @grpc_web) && error.is_a?(ConnectRPC::Error) && error.code == "resource_exhausted"
|
|
303
|
+
@output = nil
|
|
304
|
+
send_reset_stream(::Protocol::HTTP2::Error::ENHANCE_YOUR_CALM) unless closed?
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
super
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def send_headers(headers, flags = 0)
|
|
311
|
+
if @grpc || @grpc_web
|
|
312
|
+
values = {}
|
|
313
|
+
headers.each { |key, value| (values[key] ||= []) << value.to_s }
|
|
314
|
+
begin
|
|
315
|
+
GRPCProtocol.check_fields!(values)
|
|
316
|
+
rescue Error => e
|
|
317
|
+
@grpc_error = e
|
|
318
|
+
raise
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
super
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def send_body(body, trailer = nil, &)
|
|
325
|
+
# A peer reset can close the stream while deferred headers flush.
|
|
326
|
+
# Upstream otherwise starts an output task on the closed stream.
|
|
327
|
+
return body.close if closed?
|
|
328
|
+
|
|
329
|
+
@output = Output.new(self, body, trailer)
|
|
330
|
+
@output.start(&)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
class RequestStream < ::Async::HTTP::Protocol::HTTP2::Request::Stream
|
|
335
|
+
include Stream
|
|
336
|
+
|
|
337
|
+
attr_accessor :exchange
|
|
338
|
+
attr_reader :response_task
|
|
339
|
+
|
|
340
|
+
def initialize(...)
|
|
341
|
+
@cancellation_source = CancellationSource.new
|
|
342
|
+
super
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def processing_cancellation = @cancellation_source&.token
|
|
346
|
+
|
|
347
|
+
def detach_cancellation
|
|
348
|
+
@cancellation_source = nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def response_written? = @response_written && !@response_reset && (!@grpc || @grpc_terminal_written)
|
|
352
|
+
|
|
353
|
+
def send_body(...)
|
|
354
|
+
super { |task| @response_task = task }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def receive_reset_stream(frame)
|
|
358
|
+
@cancellation_source&.cancel if frame.unpack == ::Protocol::HTTP2::Error::CANCEL
|
|
359
|
+
@response_reset = true
|
|
360
|
+
super
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def send_headers(headers, flags = 0)
|
|
364
|
+
super
|
|
365
|
+
@grpc_terminal_written = true if @grpc && flags.anybits?(::Protocol::HTTP2::END_STREAM)
|
|
366
|
+
@response_written = true if flags.anybits?(::Protocol::HTTP2::END_STREAM) && !@response_reset
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def finish_output(error = nil)
|
|
370
|
+
# Raw abort has already retired the connection. Its owned tasks must
|
|
371
|
+
# finish locally rather than attempting another reset on that socket.
|
|
372
|
+
return if @connection.closed?
|
|
373
|
+
|
|
374
|
+
writable = !closed? && !@response_reset && error.nil?
|
|
375
|
+
super
|
|
376
|
+
@response_written = true if writable && !@response_reset
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
class ResponseStream < ::Async::HTTP::Protocol::HTTP2::Response::Stream
|
|
381
|
+
include Stream
|
|
382
|
+
|
|
383
|
+
attr_reader :output_task
|
|
384
|
+
|
|
385
|
+
# A raw-first destructive abort has already retired this connection.
|
|
386
|
+
# Output unwinding must not attempt another reset on its closed socket.
|
|
387
|
+
def finish_output(error = nil)
|
|
388
|
+
return if @connection.closed?
|
|
389
|
+
|
|
390
|
+
super
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def send_body(...)
|
|
394
|
+
super { |task| @output_task = task }
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
class ClientConnection < ::Async::HTTP::Protocol::HTTP2::Client
|
|
399
|
+
include Connection
|
|
400
|
+
|
|
401
|
+
def create_response
|
|
402
|
+
ResponseStream.create(self, next_stream_id).response
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Peer SETTINGS can change stream capacity for waiting acquisitions.
|
|
406
|
+
def update_remote_settings(changes)
|
|
407
|
+
super
|
|
408
|
+
ensure
|
|
409
|
+
@resource&.changed
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def receive_goaway(frame)
|
|
413
|
+
super
|
|
414
|
+
ensure
|
|
415
|
+
@resource&.unusable
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
class ServerConnection < ::Async::HTTP::Protocol::HTTP2::Server
|
|
420
|
+
include Connection
|
|
421
|
+
|
|
422
|
+
def accept_stream(stream_id)
|
|
423
|
+
::Protocol::HTTP2::Connection.instance_method(:accept_stream).bind_call(self, stream_id) do
|
|
424
|
+
RequestStream.create(self, stream_id)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# No connection-wide barrier: reset and response failures are local to
|
|
429
|
+
# this stream. Application work is a separately stoppable host task.
|
|
430
|
+
def each(task: ::Async::Task.current)
|
|
431
|
+
while (request = @requests.dequeue)
|
|
432
|
+
next if request.stream.closed?
|
|
433
|
+
|
|
434
|
+
worker = ::Async::Task.new(task, finished: false) do |_worker, incoming|
|
|
435
|
+
response = error = written = nil
|
|
436
|
+
begin
|
|
437
|
+
response = yield(incoming)
|
|
438
|
+
incoming.stream.exchange.http_status = response&.status if incoming.stream.exchange
|
|
439
|
+
unless incoming.stream.closed?
|
|
440
|
+
synchronize do
|
|
441
|
+
::Async::Task.current.defer_cancel { incoming.send_response(response) }
|
|
442
|
+
end
|
|
443
|
+
response = nil
|
|
444
|
+
# send_body schedules output; wait outside the connection guard.
|
|
445
|
+
incoming.stream.response_task&.wait
|
|
446
|
+
written = incoming.stream.response_written?
|
|
447
|
+
end
|
|
448
|
+
rescue StandardError => e
|
|
449
|
+
error = e
|
|
450
|
+
reset(incoming.stream, e)
|
|
451
|
+
ensure
|
|
452
|
+
error ||= $ERROR_INFO
|
|
453
|
+
begin
|
|
454
|
+
cleaned = close_request(response, incoming) { |failure| error ||= failure }
|
|
455
|
+
ensure
|
|
456
|
+
incoming.stream.exchange&.finish(written && cleaned && incoming.stream.response_written? &&
|
|
457
|
+
!@resource.finished?, error || $ERROR_INFO)
|
|
458
|
+
@resource.changed
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
@resource.track(worker)
|
|
463
|
+
worker.run(request)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
def close_request(response, request)
|
|
470
|
+
response&.close
|
|
471
|
+
request.body&.close
|
|
472
|
+
true
|
|
473
|
+
rescue ::Protocol::HTTP2::ProtocolError, IOError, SystemCallError => e
|
|
474
|
+
yield e if block_given?
|
|
475
|
+
nil
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def reset(stream, error)
|
|
479
|
+
code = if stream.grpc? && error.is_a?(ConnectRPC::Error) && error.code == "resource_exhausted"
|
|
480
|
+
::Protocol::HTTP2::Error::ENHANCE_YOUR_CALM
|
|
481
|
+
else
|
|
482
|
+
::Protocol::HTTP2::Error::INTERNAL_ERROR
|
|
483
|
+
end
|
|
484
|
+
stream.send_reset_stream(code) unless stream.closed?
|
|
485
|
+
rescue ::Protocol::HTTP2::ProtocolError, IOError, SystemCallError
|
|
486
|
+
nil
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
class Resource
|
|
491
|
+
attr_accessor :raw, :socket, :connection, :task, :generation, :snapshot, :ready, :draining
|
|
492
|
+
attr_reader :tasks, :progress, :activity_at
|
|
493
|
+
# Called when the connection stops accepting new streams, before any
|
|
494
|
+
# retirement join, so its owner can update accounting promptly.
|
|
495
|
+
attr_writer :on_unusable
|
|
496
|
+
|
|
497
|
+
def initialize(progress:, generation: nil, snapshot: nil)
|
|
498
|
+
@shared_progress = progress
|
|
499
|
+
@progress = ::Async::Condition.new
|
|
500
|
+
@generation = generation
|
|
501
|
+
@snapshot = snapshot
|
|
502
|
+
@tasks = []
|
|
503
|
+
@activity_at = Internal.now
|
|
504
|
+
@ready = false
|
|
505
|
+
@draining = false
|
|
506
|
+
@aborted = false
|
|
507
|
+
@finishing = false
|
|
508
|
+
@drain_task = nil
|
|
509
|
+
@retirement_members = {}
|
|
510
|
+
@cancelled_roots = {}
|
|
511
|
+
@forced_roots = {}
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def changed
|
|
515
|
+
@activity_at = Internal.now
|
|
516
|
+
@progress.signal
|
|
517
|
+
@shared_progress.signal
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def track(task)
|
|
521
|
+
raise IOError, "Connection is retiring" if @aborted
|
|
522
|
+
|
|
523
|
+
@tasks.reject!(&:finished?)
|
|
524
|
+
@tasks << task
|
|
525
|
+
task
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def empty?
|
|
529
|
+
(!@connection || @connection.streams.empty?) && @tasks.all?(&:finished?)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def finished? = @aborted
|
|
533
|
+
def finishing? = @finishing
|
|
534
|
+
def unusable = @on_unusable&.call(self)
|
|
535
|
+
|
|
536
|
+
def abort(error = IOError.new("HTTP/2 connection closed"))
|
|
537
|
+
return if @cleanup&.current?
|
|
538
|
+
|
|
539
|
+
capture_retirement
|
|
540
|
+
owned = owns_task?(::Async::Task.current)
|
|
541
|
+
launch = !@cleanup
|
|
542
|
+
if launch
|
|
543
|
+
@aborted = true
|
|
544
|
+
@abort_during_drain = true if @drain_task && !@drained
|
|
545
|
+
@cleanup = ::Async::Task.new(@lifetime, transient: true, finished: false) { retire(error) }
|
|
546
|
+
end
|
|
547
|
+
if owned
|
|
548
|
+
start_cleanup if launch
|
|
549
|
+
else
|
|
550
|
+
Internal.join_complete(@cleanup, failure: true) { start_cleanup if launch }
|
|
551
|
+
end
|
|
552
|
+
nil
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def drain(parent, expires)
|
|
556
|
+
return Internal.wait(@drain_task, expires) if @drain_task
|
|
557
|
+
|
|
558
|
+
if @aborted
|
|
559
|
+
abort
|
|
560
|
+
return true
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
@draining = true
|
|
564
|
+
unless @ready
|
|
565
|
+
abort
|
|
566
|
+
return true
|
|
567
|
+
end
|
|
568
|
+
@drain_task = ::Async::Task.new(parent, finished: false) do
|
|
569
|
+
@progress.wait until empty?
|
|
570
|
+
next !@abort_during_drain if @aborted
|
|
571
|
+
|
|
572
|
+
@finishing = true
|
|
573
|
+
@connection.defer_close = true
|
|
574
|
+
@connection.send_goaway
|
|
575
|
+
@raw.shutdown(Socket::SHUT_WR)
|
|
576
|
+
reader = @connection.reader_task
|
|
577
|
+
cancel_root(reader)
|
|
578
|
+
Internal.join_complete(reader) if reader
|
|
579
|
+
# Discard unread inbound control bytes before closing TCP, so
|
|
580
|
+
# the peer receives the complete response/GOAWAY rather than RST.
|
|
581
|
+
loop { @raw.readpartial(16_384) }
|
|
582
|
+
rescue EOFError
|
|
583
|
+
@drained = true
|
|
584
|
+
true
|
|
585
|
+
rescue StandardError
|
|
586
|
+
false
|
|
587
|
+
ensure
|
|
588
|
+
abort
|
|
589
|
+
end
|
|
590
|
+
@drain_task.run
|
|
591
|
+
Internal.wait(@drain_task, expires)
|
|
592
|
+
rescue ::Async::TimeoutError
|
|
593
|
+
abort
|
|
594
|
+
false
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
private
|
|
598
|
+
|
|
599
|
+
def retirement_parent=(parent)
|
|
600
|
+
@lifetime = parent
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def retirement_completed? = !!@retirement_completed
|
|
604
|
+
|
|
605
|
+
# Retirement starts, closing raw TCP, before the owner notification
|
|
606
|
+
# can yield; a caller cancelled in that notification cannot leave it
|
|
607
|
+
# unstarted. The notification still precedes any retirement join.
|
|
608
|
+
def start_cleanup
|
|
609
|
+
@cleanup.run
|
|
610
|
+
ensure
|
|
611
|
+
unusable
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def retirement_roots
|
|
615
|
+
outputs = (@connection ? @connection.streams.values : []).flat_map do |stream|
|
|
616
|
+
[stream.respond_to?(:response_task) ? stream.response_task : nil,
|
|
617
|
+
stream.respond_to?(:output_task) ? stream.output_task : nil]
|
|
618
|
+
end
|
|
619
|
+
[*@connection&.writers&.keys, @connection&.reader_task, *@tasks, @task, *outputs].compact.uniq
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def capture_retirement
|
|
623
|
+
retirement_roots.each { |task| capture_task(task) }
|
|
624
|
+
members = @retirement_members.keys
|
|
625
|
+
members.each { |task| capture_task(task) }
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def capture_task(task)
|
|
629
|
+
@retirement_members[task] = true
|
|
630
|
+
task.children&.to_a&.each { |child| capture_task(child) unless child.transient? }
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def owns_task?(task)
|
|
634
|
+
capture_retirement
|
|
635
|
+
while task
|
|
636
|
+
return true if @retirement_members.key?(task)
|
|
637
|
+
return false if task.transient?
|
|
638
|
+
|
|
639
|
+
task = task.parent
|
|
640
|
+
end
|
|
641
|
+
false
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def cancel_root(task)
|
|
645
|
+
return if !task || @cancelled_roots.key?(task) || task.finished?
|
|
646
|
+
|
|
647
|
+
# Record cascade coverage before cancellation can reenter or reparent.
|
|
648
|
+
mark_cancelled(task)
|
|
649
|
+
task.cancel
|
|
650
|
+
# The first request can remain deferred by the pinned frame/response
|
|
651
|
+
# guard. Deliver it now, once, rather than waiting on that guard.
|
|
652
|
+
force_deferred(task)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def force_deferred(task)
|
|
656
|
+
return if @forced_roots.key?(task) || task.finished? || !task.cancel_deferred?
|
|
657
|
+
|
|
658
|
+
@forced_roots[task] = true
|
|
659
|
+
task.cancel
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def mark_cancelled(task)
|
|
663
|
+
@cancelled_roots[task] = true
|
|
664
|
+
task.children&.to_a&.each { |child| mark_cancelled(child) unless child.transient? }
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def retire(error)
|
|
668
|
+
# Raw TCP precedes any protocol, buffered stream or TLS disposal.
|
|
669
|
+
Internal.raw_close(@raw)
|
|
670
|
+
# Best-effort scheduling hint only. A return proves no unwind or
|
|
671
|
+
# completion; the retained-task joins below establish retirement.
|
|
672
|
+
::Async::Task.current.yield
|
|
673
|
+
@connection&.close!
|
|
674
|
+
loop do
|
|
675
|
+
capture_retirement
|
|
676
|
+
writers = @connection&.writers&.keys || []
|
|
677
|
+
roots = retirement_roots
|
|
678
|
+
roots.reject! do |task|
|
|
679
|
+
ancestor = task
|
|
680
|
+
covered = false
|
|
681
|
+
until ancestor.transient? || !(ancestor = ancestor.parent)
|
|
682
|
+
covered ||= roots.include?(ancestor)
|
|
683
|
+
end
|
|
684
|
+
covered
|
|
685
|
+
end
|
|
686
|
+
roots.each { |task| cancel_root(task) }
|
|
687
|
+
# An ancestor's cascade can leave the registered frame writer with
|
|
688
|
+
# only a deferred request. Logical coverage is not forced delivery.
|
|
689
|
+
writers.each { |writer| force_deferred(writer) }
|
|
690
|
+
@retirement_members.keys.reverse_each { |task| Internal.join_complete(task) }
|
|
691
|
+
capture_retirement
|
|
692
|
+
break if @retirement_members.keys.all?(&:finished?)
|
|
693
|
+
end
|
|
694
|
+
begin
|
|
695
|
+
@connection&.finish_close(error)
|
|
696
|
+
rescue StandardError
|
|
697
|
+
nil
|
|
698
|
+
ensure
|
|
699
|
+
begin
|
|
700
|
+
@socket&.close
|
|
701
|
+
rescue StandardError
|
|
702
|
+
nil
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
@retirement_completed = true
|
|
706
|
+
changed
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|