connectrpc-falcon 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 +352 -0
- data/examples/falcon.rb +31 -0
- data/lib/connectrpc/falcon/completion_join.rb +247 -0
- data/lib/connectrpc/falcon/endpoint.rb +58 -0
- data/lib/connectrpc/falcon/environment.rb +69 -0
- data/lib/connectrpc/falcon/http1.rb +501 -0
- data/lib/connectrpc/falcon/rack_guard.rb +175 -0
- data/lib/connectrpc/falcon/server.rb +121 -0
- data/lib/connectrpc/falcon/version.rb +7 -0
- data/lib/connectrpc/falcon.rb +21 -0
- data/rbs/async_service_0_25/service.rbs +29 -0
- data/sig/connectrpc-falcon.rbs +66 -0
- data/sig/manifest.yaml +2 -0
- metadata +151 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Falcon
|
|
5
|
+
# HTTPS describes the listener without compiling TLS in the parent process.
|
|
6
|
+
class Endpoint < ::Async::HTTP::Endpoint
|
|
7
|
+
def initialize(url, endpoint = nil, **options)
|
|
8
|
+
unless %w[http https].include?(url.scheme) && url.hostname && !url.userinfo && !url.query && !url.fragment &&
|
|
9
|
+
["",
|
|
10
|
+
"/"].include?(url.path) && !options.keys.intersect?(%i[ssl_context tls_configuration scheme protocol])
|
|
11
|
+
raise ArgumentError, "Expected a raw HTTP(S) listener endpoint"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_endpoint(endpoint = nil)
|
|
18
|
+
endpoint || tcp_endpoint
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# socket_accept runs before Wrapper schedules the accepted task, including
|
|
23
|
+
# connections with no ClientHello yet. Never select a generation via SNI.
|
|
24
|
+
class AcceptWrapper < ::IO::Endpoint::Wrapper
|
|
25
|
+
def initialize(session, on_reject:)
|
|
26
|
+
super()
|
|
27
|
+
@session = session
|
|
28
|
+
@on_reject = on_reject
|
|
29
|
+
@listeners = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def socket_accept(server)
|
|
33
|
+
@listeners[::Async::Task.current] = true
|
|
34
|
+
loop do
|
|
35
|
+
raw, address = super
|
|
36
|
+
accepted = @session.accept(raw)
|
|
37
|
+
return [accepted, address] if accepted
|
|
38
|
+
|
|
39
|
+
@on_reject.call
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def schedule(&)
|
|
44
|
+
if @listeners.key?(::Async::Task.current)
|
|
45
|
+
@session.schedule(&)
|
|
46
|
+
else
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def stop
|
|
52
|
+
@listeners.each_key(&:stop)
|
|
53
|
+
@listeners.clear
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_constant :AcceptWrapper
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module ConnectRPC
|
|
6
|
+
module Falcon
|
|
7
|
+
class Service < ::Falcon::Service::Server
|
|
8
|
+
def run(_instance, evaluator, listener)
|
|
9
|
+
# This enclosing task's ensure executes on Falcon's ordinary managed
|
|
10
|
+
# Interrupt route before its children are cancelled by the container.
|
|
11
|
+
ready = ::Async::Queue.new
|
|
12
|
+
::Async::Task.current.async do |task|
|
|
13
|
+
server = evaluator.make_server(listener.endpoint)
|
|
14
|
+
server.run
|
|
15
|
+
ready.enqueue([server, nil])
|
|
16
|
+
announced = true
|
|
17
|
+
task.children&.to_a&.each(&:wait)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
raise if announced
|
|
20
|
+
|
|
21
|
+
ready.enqueue([nil, e])
|
|
22
|
+
ensure
|
|
23
|
+
if $ERROR_INFO && !managed_stop?($ERROR_INFO)
|
|
24
|
+
server&.abort
|
|
25
|
+
else
|
|
26
|
+
server&.close
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
server, error = ready.dequeue
|
|
30
|
+
raise error if error
|
|
31
|
+
|
|
32
|
+
# Service::Server keeps this for its normal statistics/process title,
|
|
33
|
+
# and cannot declare readiness until the worker's initial load succeeds.
|
|
34
|
+
server
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def managed_stop?(error)
|
|
40
|
+
while error
|
|
41
|
+
return true if error.is_a?(Interrupt)
|
|
42
|
+
|
|
43
|
+
error = error.cause
|
|
44
|
+
end
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Environment
|
|
50
|
+
include ::Falcon::Environment::Server
|
|
51
|
+
|
|
52
|
+
def service_class = Service
|
|
53
|
+
def endpoint = Endpoint.parse(url, **endpoint_options)
|
|
54
|
+
def rack_app = nil
|
|
55
|
+
def tls = nil
|
|
56
|
+
def tls_provider = nil
|
|
57
|
+
def server_options = {}
|
|
58
|
+
|
|
59
|
+
def connectrpc_server
|
|
60
|
+
raise NotImplementedError, "Define the worker-local connectrpc_server"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def make_server(bound)
|
|
64
|
+
Server.new(server: connectrpc_server, endpoint: bound, rack_app: rack_app, tls: tls,
|
|
65
|
+
tls_provider: tls_provider, utilization_registry: self[:utilization_registry], **server_options)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require_relative "completion_join"
|
|
5
|
+
|
|
6
|
+
require "async/http/protocol/http1/server"
|
|
7
|
+
|
|
8
|
+
module ConnectRPC
|
|
9
|
+
module Falcon
|
|
10
|
+
# The pinned HTTP/1 writer flushes framing after body EOF, before close
|
|
11
|
+
# callbacks. Track that state without inspecting application payload bytes.
|
|
12
|
+
class HTTP1ResponseBody < ::Protocol::HTTP::Body::Wrapper
|
|
13
|
+
attr_reader :eof
|
|
14
|
+
|
|
15
|
+
def read
|
|
16
|
+
chunk = super
|
|
17
|
+
@eof = true if chunk.nil?
|
|
18
|
+
chunk
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def empty?
|
|
22
|
+
empty = super
|
|
23
|
+
@eof = true if empty
|
|
24
|
+
empty
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
private_constant :HTTP1ResponseBody
|
|
28
|
+
|
|
29
|
+
class HTTP1Connection < ::Async::HTTP::Protocol::HTTP1::Server
|
|
30
|
+
attr_accessor :resource
|
|
31
|
+
attr_writer :rejected_input, :retiring_raw
|
|
32
|
+
|
|
33
|
+
def close_read
|
|
34
|
+
# The framing parser can close input itself before raising EOFError.
|
|
35
|
+
@rejected_input&.abort_read
|
|
36
|
+
@retiring_raw.close if @retiring_raw && !@retiring_raw.closed?
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def next_request
|
|
41
|
+
return if @resource.draining || @resource.finished?
|
|
42
|
+
|
|
43
|
+
request = super
|
|
44
|
+
if request
|
|
45
|
+
fields = request.headers.header
|
|
46
|
+
if fields.any? do |key, value|
|
|
47
|
+
key.casecmp?("content-type") && value.downcase.start_with?("application/grpc-web")
|
|
48
|
+
end &&
|
|
49
|
+
fields.sum { |key, value| key.bytesize + value.bytesize + 32 } > GRPCProtocol::FIELD_LIMIT
|
|
50
|
+
raise Error.new(:resource_exhausted, "Web field block exceeds byte limit"), cause: nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
# Retirement can happen while the upstream parser is blocked.
|
|
54
|
+
return if @resource.draining || @resource.finished?
|
|
55
|
+
|
|
56
|
+
@resource.active = true if request
|
|
57
|
+
@resource.changed
|
|
58
|
+
request
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def each(...)
|
|
62
|
+
super
|
|
63
|
+
ensure
|
|
64
|
+
# Header failures and cancellation can bypass write_body entirely.
|
|
65
|
+
begin
|
|
66
|
+
@resource.exchange&.finish(false, $ERROR_INFO)
|
|
67
|
+
ensure
|
|
68
|
+
@resource.exchange = nil
|
|
69
|
+
begin
|
|
70
|
+
@resource.input_retirement&.close(false)
|
|
71
|
+
ensure
|
|
72
|
+
@resource.input_retirement = nil
|
|
73
|
+
@resource.failed! if @resource.active
|
|
74
|
+
@resource.active = false
|
|
75
|
+
@resource.changed
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Match the pinned writer's positional arguments.
|
|
81
|
+
# rubocop:disable-next Style/OptionalBooleanParameter
|
|
82
|
+
def write_body(version, body, head = false, trailer = nil)
|
|
83
|
+
if @resource.input_retirement
|
|
84
|
+
@response_eof = head || body.nil?
|
|
85
|
+
body = @response_body = HTTP1ResponseBody.new(body) if body
|
|
86
|
+
end
|
|
87
|
+
super
|
|
88
|
+
written = true
|
|
89
|
+
rescue StandardError
|
|
90
|
+
@resource.failed!
|
|
91
|
+
raise
|
|
92
|
+
ensure
|
|
93
|
+
@response_body = nil
|
|
94
|
+
@response_eof = false
|
|
95
|
+
# Upstream returns after final fixed/chunked bytes AND body callbacks.
|
|
96
|
+
begin
|
|
97
|
+
@resource.exchange&.finish(!!written, $ERROR_INFO)
|
|
98
|
+
ensure
|
|
99
|
+
begin
|
|
100
|
+
@resource.input_retirement&.close(!!written)
|
|
101
|
+
ensure
|
|
102
|
+
@resource.input_retirement = nil
|
|
103
|
+
@resource.exchange = nil
|
|
104
|
+
@resource.active = false
|
|
105
|
+
@resource.changed
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def flush_stream
|
|
113
|
+
result = super
|
|
114
|
+
if @response_eof || @response_body&.eof
|
|
115
|
+
@resource.input_retirement&.final_flushed
|
|
116
|
+
@response_eof = false
|
|
117
|
+
@response_body = nil
|
|
118
|
+
end
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_constant :HTTP1Connection
|
|
123
|
+
|
|
124
|
+
# Connect-only transfer from the original processor to the connection task.
|
|
125
|
+
# Framing completion never relies on empty?, which also becomes true on error.
|
|
126
|
+
class HTTP1StreamInput
|
|
127
|
+
attr_writer :input
|
|
128
|
+
|
|
129
|
+
def initialize(body, raw:, connection:)
|
|
130
|
+
@body = body
|
|
131
|
+
@framed = body&.body
|
|
132
|
+
@raw = raw
|
|
133
|
+
@connection = connection
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def on_close(&completion)
|
|
137
|
+
@completion = completion
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def framed_complete?
|
|
141
|
+
case @framed
|
|
142
|
+
when nil then true
|
|
143
|
+
when ::Protocol::HTTP1::Body::Fixed then @framed.remaining.zero?
|
|
144
|
+
when ::Protocol::HTTP1::Body::Chunked then !@framed.length.nil?
|
|
145
|
+
else false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def head_resolved(status)
|
|
150
|
+
@retiring = status == 505 || !framed_complete?
|
|
151
|
+
@connection.persistent = false if @retiring
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def processor_finished
|
|
155
|
+
@discarded = @input&.release_remainder || 0
|
|
156
|
+
@input = nil
|
|
157
|
+
@body&.close if framed_complete?
|
|
158
|
+
@processed = true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def final_flushed
|
|
162
|
+
return if @close_expires
|
|
163
|
+
|
|
164
|
+
@close_expires = now + 0.5
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def close(delivered)
|
|
168
|
+
return if @closed
|
|
169
|
+
|
|
170
|
+
@closed = true
|
|
171
|
+
begin
|
|
172
|
+
if delivered && @processed && @retiring
|
|
173
|
+
retire if @close_expires
|
|
174
|
+
elsif !delivered || !@processed
|
|
175
|
+
abort_read
|
|
176
|
+
end
|
|
177
|
+
rescue StandardError
|
|
178
|
+
# Hygiene cannot change an already observed RPC delivery outcome.
|
|
179
|
+
abort_read
|
|
180
|
+
ensure
|
|
181
|
+
begin
|
|
182
|
+
# A retired connection is never reused, even after complete framing.
|
|
183
|
+
abort_read if @retiring || !delivered || !@processed
|
|
184
|
+
ensure
|
|
185
|
+
begin
|
|
186
|
+
@body&.close
|
|
187
|
+
ensure
|
|
188
|
+
@completion&.call
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def retire
|
|
197
|
+
# HTTP completion is already observed. TLS cleanup must not precede
|
|
198
|
+
# this raw TCP half-close or extend the one final-flush budget.
|
|
199
|
+
@raw.shutdown(Socket::SHUT_WR)
|
|
200
|
+
begin
|
|
201
|
+
remaining = @close_expires - now
|
|
202
|
+
::Async::Task.current.with_timeout(remaining) { discard } if remaining.positive?
|
|
203
|
+
rescue StandardError
|
|
204
|
+
# A late framing/timeout failure cannot revise completed delivery.
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
remaining = @close_expires - now
|
|
208
|
+
::Async::Task.current.sleep(remaining) if remaining.positive? && !@raw.closed?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def discard
|
|
212
|
+
# Chunked EOF closes the buffered/TLS read side before raising. Guard
|
|
213
|
+
# only post-delivery reads, without recursively closing the connection.
|
|
214
|
+
@connection.retiring_raw = @raw
|
|
215
|
+
expires = now + 0.1
|
|
216
|
+
until framed_complete?
|
|
217
|
+
return if 262_144 - @discarded < 65_536
|
|
218
|
+
|
|
219
|
+
remaining = expires - now
|
|
220
|
+
return if remaining <= 0
|
|
221
|
+
|
|
222
|
+
# Pinned framed reads have no length argument and return <=64 KiB.
|
|
223
|
+
# Reserve a whole block before each read, including adapter remainder.
|
|
224
|
+
chunk = ::Async::Task.current.with_timeout(remaining) { @body.read }
|
|
225
|
+
@discarded += chunk.bytesize if chunk
|
|
226
|
+
end
|
|
227
|
+
ensure
|
|
228
|
+
@connection.retiring_raw = nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def abort_read
|
|
232
|
+
@raw.close unless @raw.closed?
|
|
233
|
+
# Finishable.close must still resolve after raw-first teardown. Closing
|
|
234
|
+
# connection state first prevents framed close_read on an already shut IO.
|
|
235
|
+
@connection.close
|
|
236
|
+
rescue IOError, SystemCallError
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
241
|
+
end
|
|
242
|
+
private_constant :HTTP1StreamInput
|
|
243
|
+
|
|
244
|
+
# One request's original framing and deadline, owned only by native Web H1.
|
|
245
|
+
class HTTP1RejectedInput
|
|
246
|
+
def initialize(body, raw:, connection:, headers:)
|
|
247
|
+
@body = body
|
|
248
|
+
@raw = raw
|
|
249
|
+
@connection = connection
|
|
250
|
+
# Capture before processing. Invalid fields remain core's validation
|
|
251
|
+
# responsibility and must not change its validation order.
|
|
252
|
+
@deadline = GRPCProtocol.request_deadline(headers)
|
|
253
|
+
rescue Error
|
|
254
|
+
@deadline = nil
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def close(error = $ERROR_INFO)
|
|
258
|
+
return if @closed
|
|
259
|
+
|
|
260
|
+
@closed = true
|
|
261
|
+
begin
|
|
262
|
+
@connection.rejected_input = self
|
|
263
|
+
raise error if error
|
|
264
|
+
|
|
265
|
+
if @body && !@body.empty?
|
|
266
|
+
@expires = now + 0.1
|
|
267
|
+
@expires = [@expires, now + @deadline.remaining].min if @deadline
|
|
268
|
+
until @body.empty?
|
|
269
|
+
remaining = @expires - now
|
|
270
|
+
raise ::Async::TimeoutError, "Rejected Web input did not finish" if remaining <= 0
|
|
271
|
+
|
|
272
|
+
# Pinned Fixed/Chunked reads are <=64 KiB and preserve framing,
|
|
273
|
+
# including a core input adapter's discarded readahead remainder.
|
|
274
|
+
::Async::Task.current.with_timeout(remaining) { @body.read }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
finished = true
|
|
278
|
+
ensure
|
|
279
|
+
# Cancellation and malformed/expired input must fail raw TCP before
|
|
280
|
+
# Finishable/body or TLS cleanup can yield. Never signal RPC cancellation.
|
|
281
|
+
begin
|
|
282
|
+
abort_read unless finished
|
|
283
|
+
ensure
|
|
284
|
+
begin
|
|
285
|
+
@body&.close
|
|
286
|
+
ensure
|
|
287
|
+
@connection.rejected_input = nil
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def abort_read
|
|
294
|
+
@raw.close unless @raw.closed?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
300
|
+
end
|
|
301
|
+
private_constant :HTTP1RejectedInput
|
|
302
|
+
|
|
303
|
+
# Serial HTTP/1 ownership. It deliberately has no HTTP/2 stream/GOAWAY API.
|
|
304
|
+
class HTTP1Resource
|
|
305
|
+
attr_accessor :raw, :socket, :task, :snapshot, :ready, :draining, :active, :exchange, :input_retirement
|
|
306
|
+
attr_reader :connection, :generation, :progress, :activity_at
|
|
307
|
+
|
|
308
|
+
def initialize(stream, progress:, generation:)
|
|
309
|
+
@connection = HTTP1Connection.new(stream, "HTTP/1.1")
|
|
310
|
+
@connection.resource = self
|
|
311
|
+
@shared_progress = progress
|
|
312
|
+
@progress = ::Async::Condition.new
|
|
313
|
+
@generation = generation
|
|
314
|
+
@tasks = []
|
|
315
|
+
@retirement_members = {}
|
|
316
|
+
@cancelled_roots = {}
|
|
317
|
+
@activity_at = now
|
|
318
|
+
@active = @ready = @draining = @aborted = @finishing = false
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def changed
|
|
322
|
+
@activity_at = now
|
|
323
|
+
@progress.signal
|
|
324
|
+
@shared_progress.signal
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def track(task)
|
|
328
|
+
raise IOError, "Connection is retiring" if @aborted
|
|
329
|
+
|
|
330
|
+
@tasks.reject!(&:finished?)
|
|
331
|
+
@tasks << task
|
|
332
|
+
task
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def empty? = !@active && @tasks.all?(&:finished?)
|
|
336
|
+
def finished? = @aborted
|
|
337
|
+
def finishing? = @finishing || @draining
|
|
338
|
+
|
|
339
|
+
def failed!
|
|
340
|
+
@failed = true
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def web_input(body, headers:)
|
|
344
|
+
HTTP1RejectedInput.new(body, raw: @raw, connection: @connection, headers: headers)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def prepare_stream_input(body)
|
|
348
|
+
@input_retirement = HTTP1StreamInput.new(body, raw: @raw, connection: @connection)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def abort
|
|
352
|
+
return if @cleanup&.current?
|
|
353
|
+
|
|
354
|
+
capture_retirement
|
|
355
|
+
owned = owns_task?(::Async::Task.current)
|
|
356
|
+
launch = !@cleanup
|
|
357
|
+
if launch
|
|
358
|
+
@aborted = true
|
|
359
|
+
@cleanup = ::Async::Task.new(@lifetime, transient: true, finished: false) { retire }
|
|
360
|
+
end
|
|
361
|
+
if owned
|
|
362
|
+
@cleanup.run if launch
|
|
363
|
+
else
|
|
364
|
+
join(@cleanup, failure: true) { @cleanup.run if launch }
|
|
365
|
+
end
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def drain(parent, expires)
|
|
370
|
+
return wait(@drain_task, expires) if @drain_task
|
|
371
|
+
|
|
372
|
+
if @aborted
|
|
373
|
+
abort
|
|
374
|
+
return true
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
@draining = true
|
|
378
|
+
unless @ready
|
|
379
|
+
abort
|
|
380
|
+
return true
|
|
381
|
+
end
|
|
382
|
+
@drain_task = parent.async(finished: false) do
|
|
383
|
+
@progress.wait until empty? || @aborted
|
|
384
|
+
next false if @aborted || @failed
|
|
385
|
+
|
|
386
|
+
@finishing = true
|
|
387
|
+
# Cancel an idle parser only after the admitted response and callback
|
|
388
|
+
# complete. Raw half-close preserves the HTTP wire-completion boundary.
|
|
389
|
+
@raw.shutdown(Socket::SHUT_WR)
|
|
390
|
+
cancel_root(@task)
|
|
391
|
+
join(@task)
|
|
392
|
+
loop { @raw.readpartial(16_384) }
|
|
393
|
+
rescue EOFError
|
|
394
|
+
true
|
|
395
|
+
rescue StandardError
|
|
396
|
+
false
|
|
397
|
+
ensure
|
|
398
|
+
abort
|
|
399
|
+
end
|
|
400
|
+
wait(@drain_task, expires)
|
|
401
|
+
rescue ::Async::TimeoutError
|
|
402
|
+
abort
|
|
403
|
+
false
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
private
|
|
407
|
+
|
|
408
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
409
|
+
|
|
410
|
+
def wait(task, expires)
|
|
411
|
+
remaining = expires - now
|
|
412
|
+
raise ::Async::TimeoutError if remaining <= 0
|
|
413
|
+
|
|
414
|
+
::Async::Task.current.with_timeout(remaining) { task.wait }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def retirement_parent=(parent)
|
|
418
|
+
@lifetime = parent
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def retirement_completed? = !!@retirement_completed
|
|
422
|
+
|
|
423
|
+
def retirement_roots = [*@tasks, @task].compact.uniq
|
|
424
|
+
|
|
425
|
+
def capture_retirement
|
|
426
|
+
retirement_roots.each { |task| capture_task(task) }
|
|
427
|
+
members = @retirement_members.keys
|
|
428
|
+
members.each { |task| capture_task(task) }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def capture_task(task)
|
|
432
|
+
@retirement_members[task] = true
|
|
433
|
+
task.children&.to_a&.each { |child| capture_task(child) unless child.transient? }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def owns_task?(task)
|
|
437
|
+
capture_retirement
|
|
438
|
+
while task
|
|
439
|
+
return true if @retirement_members.key?(task)
|
|
440
|
+
return false if task.transient?
|
|
441
|
+
|
|
442
|
+
task = task.parent
|
|
443
|
+
end
|
|
444
|
+
false
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def cancel_root(task)
|
|
448
|
+
return if !task || @cancelled_roots.key?(task) || task.finished?
|
|
449
|
+
|
|
450
|
+
mark_cancelled(task)
|
|
451
|
+
task.cancel
|
|
452
|
+
# Pinned HTTP/1 defers cancellation through response/body callbacks.
|
|
453
|
+
# A deferred request still needs its one immediate forced delivery.
|
|
454
|
+
task.cancel if task.cancel_deferred?
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def mark_cancelled(task)
|
|
458
|
+
@cancelled_roots[task] = true
|
|
459
|
+
task.children&.to_a&.each { |child| mark_cancelled(child) unless child.transient? }
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def retire
|
|
463
|
+
@raw.close unless @raw.closed?
|
|
464
|
+
# Best-effort scheduling hint only. A return proves no unwind or
|
|
465
|
+
# completion; the retained-task joins below establish retirement.
|
|
466
|
+
::Async::Task.current.yield
|
|
467
|
+
loop do
|
|
468
|
+
capture_retirement
|
|
469
|
+
roots = retirement_roots
|
|
470
|
+
roots.reject! do |task|
|
|
471
|
+
ancestor = task
|
|
472
|
+
covered = false
|
|
473
|
+
until ancestor.transient? || !(ancestor = ancestor.parent)
|
|
474
|
+
covered ||= roots.include?(ancestor)
|
|
475
|
+
end
|
|
476
|
+
covered
|
|
477
|
+
end
|
|
478
|
+
roots.each { |task| cancel_root(task) }
|
|
479
|
+
@retirement_members.keys.reverse_each { |task| join(task) }
|
|
480
|
+
capture_retirement
|
|
481
|
+
break if @retirement_members.keys.all?(&:finished?)
|
|
482
|
+
end
|
|
483
|
+
begin
|
|
484
|
+
@connection.close
|
|
485
|
+
rescue StandardError
|
|
486
|
+
nil
|
|
487
|
+
end
|
|
488
|
+
@retirement_completed = true
|
|
489
|
+
changed
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Local counterpart of the private Async helper; no shared public utility.
|
|
493
|
+
def join(task, failure: false)
|
|
494
|
+
return unless task
|
|
495
|
+
|
|
496
|
+
CompletionJoin.call(task, failure: failure) { yield if block_given? }
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
private_constant :HTTP1Resource
|
|
500
|
+
end
|
|
501
|
+
end
|